Merge branch 'staging' into cc-async-count

e4dc4896094ad7efe65ea05228cbedb1178c6b03

Cohee <18619528+Cohee1207@users.noreply.github.com>

75 files changed, +1579 -1385Ignore whitespace
.eslintrc.js+3 -0
@@ -16,6 +16,9 @@ module.exports = {
16 env: {16 env: {
17 node: true,17 node: true,
18 },18 },
19 parserOptions: {
20 sourceType: 'module',
21 },
19 },22 },
20 {23 {
21 files: ['src/**/*.mjs'],24 files: ['src/**/*.mjs'],
jsconfig.json+1 -1
@@ -2,7 +2,7 @@
2 "compilerOptions": {2 "compilerOptions": {
3 "module": "ESNext",3 "module": "ESNext",
4 "target": "ESNext",4 "target": "ESNext",
5 "moduleResolution": "node",5 "moduleResolution": "Node",
6 "strictNullChecks": true,6 "strictNullChecks": true,
7 "strictFunctionTypes": true,7 "strictFunctionTypes": true,
8 "checkJs": true,8 "checkJs": true,
package-lock.json+431 -40
@@ -24,9 +24,9 @@
24 "csrf-csrf": "^2.2.3",24 "csrf-csrf": "^2.2.3",
25 "express": "^4.21.0",25 "express": "^4.21.0",
26 "form-data": "^4.0.0",26 "form-data": "^4.0.0",
27 "google-translate-api-browser": "^3.0.1",27 "google-translate-api-x": "^10.7.1",
28 "he": "^1.2.0",
29 "helmet": "^7.1.0",28 "helmet": "^7.1.0",
29 "html-entities": "^2.5.2",
30 "iconv-lite": "^0.6.3",30 "iconv-lite": "^0.6.3",
31 "ip-matching": "^2.1.2",31 "ip-matching": "^2.1.2",
32 "ipaddr.js": "^2.0.1",32 "ipaddr.js": "^2.0.1",
@@ -34,7 +34,7 @@
34 "lodash": "^4.17.21",34 "lodash": "^4.17.21",
35 "mime-types": "^2.1.35",35 "mime-types": "^2.1.35",
36 "multer": "^1.4.5-lts.1",36 "multer": "^1.4.5-lts.1",
37 "node-fetch": "^2.6.11",37 "node-fetch": "^3.3.2",
38 "node-persist": "^4.0.1",38 "node-persist": "^4.0.1",
39 "open": "^8.4.2",39 "open": "^8.4.2",
40 "png-chunk-text": "^1.0.0",40 "png-chunk-text": "^1.0.0",
@@ -59,9 +59,28 @@
59 "sillytavern": "server.js"59 "sillytavern": "server.js"
60 },60 },
61 "devDependencies": {61 "devDependencies": {
62 "@types/archiver": "^6.0.2",
63 "@types/command-exists": "^1.2.3",
64 "@types/compression": "^1.7.5",
65 "@types/cookie-parser": "^1.4.7",
66 "@types/cookie-session": "^2.0.49",
67 "@types/cors": "^2.8.17",
62 "@types/dompurify": "^3.0.5",68 "@types/dompurify": "^3.0.5",
69 "@types/express": "^4.17.21",
63 "@types/jquery": "^3.5.29",70 "@types/jquery": "^3.5.29",
71 "@types/lodash": "^4.17.10",
72 "@types/mime-types": "^2.1.4",
73 "@types/multer": "^1.4.12",
74 "@types/node": "^18.19.55",
75 "@types/node-persist": "^3.1.8",
76 "@types/png-chunk-text": "^1.0.3",
77 "@types/png-chunks-encode": "^1.0.2",
78 "@types/png-chunks-extract": "^1.0.2",
79 "@types/response-time": "^2.3.8",
64 "@types/toastr": "^2.1.43",80 "@types/toastr": "^2.1.43",
81 "@types/write-file-atomic": "^4.0.3",
82 "@types/yargs": "^17.0.33",
83 "@types/yauzl": "^2.10.3",
65 "eslint": "^8.57.0"84 "eslint": "^8.57.0"
66 },85 },
67 "engines": {86 "engines": {
@@ -949,6 +968,27 @@
949 "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",968 "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
950 "license": "MIT"969 "license": "MIT"
951 },970 },
971 "node_modules/@types/archiver": {
972 "version": "6.0.2",
973 "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
974 "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
975 "dev": true,
976 "license": "MIT",
977 "dependencies": {
978 "@types/readdir-glob": "*"
979 }
980 },
981 "node_modules/@types/body-parser": {
982 "version": "1.19.5",
983 "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
984 "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
985 "dev": true,
986 "license": "MIT",
987 "dependencies": {
988 "@types/connect": "*",
989 "@types/node": "*"
990 }
991 },
952 "node_modules/@types/cacheable-request": {992 "node_modules/@types/cacheable-request": {
953 "version": "6.0.3",993 "version": "6.0.3",
954 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",994 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -961,6 +1001,64 @@
961 "@types/responselike": "^1.0.0"1001 "@types/responselike": "^1.0.0"
962 }1002 }
963 },1003 },
1004 "node_modules/@types/command-exists": {
1005 "version": "1.2.3",
1006 "resolved": "https://registry.npmjs.org/@types/command-exists/-/command-exists-1.2.3.tgz",
1007 "integrity": "sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==",
1008 "dev": true,
1009 "license": "MIT"
1010 },
1011 "node_modules/@types/compression": {
1012 "version": "1.7.5",
1013 "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz",
1014 "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==",
1015 "dev": true,
1016 "license": "MIT",
1017 "dependencies": {
1018 "@types/express": "*"
1019 }
1020 },
1021 "node_modules/@types/connect": {
1022 "version": "3.4.38",
1023 "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
1024 "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
1025 "dev": true,
1026 "license": "MIT",
1027 "dependencies": {
1028 "@types/node": "*"
1029 }
1030 },
1031 "node_modules/@types/cookie-parser": {
1032 "version": "1.4.7",
1033 "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
1034 "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
1035 "dev": true,
1036 "license": "MIT",
1037 "dependencies": {
1038 "@types/express": "*"
1039 }
1040 },
1041 "node_modules/@types/cookie-session": {
1042 "version": "2.0.49",
1043 "resolved": "https://registry.npmjs.org/@types/cookie-session/-/cookie-session-2.0.49.tgz",
1044 "integrity": "sha512-4E/bBjlqLhU5l4iGPR+NkVJH593hpNsT4dC3DJDr+ODm6Qpe13kZQVkezRIb+TYDXaBMemS3yLQ+0leba3jlkQ==",
1045 "dev": true,
1046 "license": "MIT",
1047 "dependencies": {
1048 "@types/express": "*",
1049 "@types/keygrip": "*"
1050 }
1051 },
1052 "node_modules/@types/cors": {
1053 "version": "2.8.17",
1054 "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
1055 "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
1056 "dev": true,
1057 "license": "MIT",
1058 "dependencies": {
1059 "@types/node": "*"
1060 }
1061 },
964 "node_modules/@types/dompurify": {1062 "node_modules/@types/dompurify": {
965 "version": "3.0.5",1063 "version": "3.0.5",
966 "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",1064 "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
@@ -971,12 +1069,43 @@
971 "@types/trusted-types": "*"1069 "@types/trusted-types": "*"
972 }1070 }
973 },1071 },
1072 "node_modules/@types/express": {
1073 "version": "4.17.21",
1074 "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
1075 "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
1076 "dev": true,
1077 "dependencies": {
1078 "@types/body-parser": "*",
1079 "@types/express-serve-static-core": "^4.17.33",
1080 "@types/qs": "*",
1081 "@types/serve-static": "*"
1082 }
1083 },
1084 "node_modules/@types/express-serve-static-core": {
1085 "version": "4.19.6",
1086 "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
1087 "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
1088 "dev": true,
1089 "dependencies": {
1090 "@types/node": "*",
1091 "@types/qs": "*",
1092 "@types/range-parser": "*",
1093 "@types/send": "*"
1094 }
1095 },
974 "node_modules/@types/http-cache-semantics": {1096 "node_modules/@types/http-cache-semantics": {
975 "version": "4.0.2",1097 "version": "4.0.2",
976 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",1098 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",
977 "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==",1099 "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==",
978 "license": "MIT"1100 "license": "MIT"
979 },1101 },
1102 "node_modules/@types/http-errors": {
1103 "version": "2.0.4",
1104 "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
1105 "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
1106 "dev": true,
1107 "license": "MIT"
1108 },
980 "node_modules/@types/jquery": {1109 "node_modules/@types/jquery": {
981 "version": "3.5.31",1110 "version": "3.5.31",
982 "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",1111 "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
@@ -987,6 +1116,13 @@
987 "@types/sizzle": "*"1116 "@types/sizzle": "*"
988 }1117 }
989 },1118 },
1119 "node_modules/@types/keygrip": {
1120 "version": "1.0.6",
1121 "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
1122 "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==",
1123 "dev": true,
1124 "license": "MIT"
1125 },
990 "node_modules/@types/keyv": {1126 "node_modules/@types/keyv": {
991 "version": "3.1.4",1127 "version": "3.1.4",
992 "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",1128 "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
@@ -996,28 +1132,126 @@
996 "@types/node": "*"1132 "@types/node": "*"
997 }1133 }
998 },1134 },
1135 "node_modules/@types/lodash": {
1136 "version": "4.17.10",
1137 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
1138 "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
1139 "dev": true,
1140 "license": "MIT"
1141 },
999 "node_modules/@types/long": {1142 "node_modules/@types/long": {
1000 "version": "4.0.2",1143 "version": "4.0.2",
1001 "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",1144 "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
1002 "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",1145 "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
1003 "license": "MIT"1146 "license": "MIT"
1004 },1147 },
1005 "node_modules/@types/node": {1148 "node_modules/@types/mime": {
1006 "version": "16.9.1",1149 "version": "1.3.5",
1007 "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",1150 "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
1008 "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",1151 "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
1152 "dev": true,
1153 "license": "MIT"
1154 },
1155 "node_modules/@types/mime-types": {
1156 "version": "2.1.4",
1157 "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
1158 "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
1159 "dev": true,
1009 "license": "MIT"1160 "license": "MIT"
1010 },1161 },
1162 "node_modules/@types/multer": {
1163 "version": "1.4.12",
1164 "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
1165 "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
1166 "dev": true,
1167 "license": "MIT",
1168 "dependencies": {
1169 "@types/express": "*"
1170 }
1171 },
1172 "node_modules/@types/node": {
1173 "version": "18.19.55",
1174 "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz",
1175 "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==",
1176 "license": "MIT",
1177 "dependencies": {
1178 "undici-types": "~5.26.4"
1179 }
1180 },
1011 "node_modules/@types/node-fetch": {1181 "node_modules/@types/node-fetch": {
1012 "version": "2.6.9",1182 "version": "2.6.11",
1013 "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz",1183 "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
1014 "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==",1184 "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
1015 "license": "MIT",1185 "license": "MIT",
1016 "dependencies": {1186 "dependencies": {
1017 "@types/node": "*",1187 "@types/node": "*",
1018 "form-data": "^4.0.0"1188 "form-data": "^4.0.0"
1019 }1189 }
1020 },1190 },
1191 "node_modules/@types/node-persist": {
1192 "version": "3.1.8",
1193 "resolved": "https://registry.npmjs.org/@types/node-persist/-/node-persist-3.1.8.tgz",
1194 "integrity": "sha512-QLidg6/SadZYPrTKxtxL1A85XBoQlG40bhoMdhu6DH6+eNCMr2j+RGfFZ9I9+IY8W/PDwQonJ+iBWD62jZjMfg==",
1195 "dev": true,
1196 "license": "MIT",
1197 "dependencies": {
1198 "@types/node": "*"
1199 }
1200 },
1201 "node_modules/@types/png-chunk-text": {
1202 "version": "1.0.3",
1203 "resolved": "https://registry.npmjs.org/@types/png-chunk-text/-/png-chunk-text-1.0.3.tgz",
1204 "integrity": "sha512-7keEFz73uNJ9Ar1XMCNnHEXT9pICJnouMQCCYgBEmHMgdkXaQzSTmSvr6tUDSqgdEgmlRAxZd97wprgliyZoCg==",
1205 "dev": true,
1206 "license": "MIT"
1207 },
1208 "node_modules/@types/png-chunks-encode": {
1209 "version": "1.0.2",
1210 "resolved": "https://registry.npmjs.org/@types/png-chunks-encode/-/png-chunks-encode-1.0.2.tgz",
1211 "integrity": "sha512-Dxn0aXEcSg1wVeHjvNlygm/+fKBDzWMCdxJYhjGUTeefFW/jYxWcrg+W7ppLBfH44iJMqeVBHtHBwtYQUeYvgw==",
1212 "dev": true,
1213 "license": "MIT"
1214 },
1215 "node_modules/@types/png-chunks-extract": {
1216 "version": "1.0.2",
1217 "resolved": "https://registry.npmjs.org/@types/png-chunks-extract/-/png-chunks-extract-1.0.2.tgz",
1218 "integrity": "sha512-z6djfFIbrrddtunoMJBOPlyZrnmeuG1kkvHUNi2QfpOb+JMMLuLliHHTmMyRi7k7LiTAut0HbdGCF6ibDtQAHQ==",
1219 "dev": true,
1220 "license": "MIT"
1221 },
1222 "node_modules/@types/qs": {
1223 "version": "6.9.16",
1224 "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
1225 "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
1226 "dev": true
1227 },
1228 "node_modules/@types/range-parser": {
1229 "version": "1.2.7",
1230 "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
1231 "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
1232 "dev": true
1233 },
1234 "node_modules/@types/readdir-glob": {
1235 "version": "1.1.5",
1236 "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
1237 "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
1238 "dev": true,
1239 "license": "MIT",
1240 "dependencies": {
1241 "@types/node": "*"
1242 }
1243 },
1244 "node_modules/@types/response-time": {
1245 "version": "2.3.8",
1246 "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz",
1247 "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==",
1248 "dev": true,
1249 "license": "MIT",
1250 "dependencies": {
1251 "@types/express": "*",
1252 "@types/node": "*"
1253 }
1254 },
1021 "node_modules/@types/responselike": {1255 "node_modules/@types/responselike": {
1022 "version": "1.0.1",1256 "version": "1.0.1",
1023 "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",1257 "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",
@@ -1027,6 +1261,29 @@
1027 "@types/node": "*"1261 "@types/node": "*"
1028 }1262 }
1029 },1263 },
1264 "node_modules/@types/send": {
1265 "version": "0.17.4",
1266 "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
1267 "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
1268 "dev": true,
1269 "license": "MIT",
1270 "dependencies": {
1271 "@types/mime": "^1",
1272 "@types/node": "*"
1273 }
1274 },
1275 "node_modules/@types/serve-static": {
1276 "version": "1.15.7",
1277 "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
1278 "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
1279 "dev": true,
1280 "license": "MIT",
1281 "dependencies": {
1282 "@types/http-errors": "*",
1283 "@types/node": "*",
1284 "@types/send": "*"
1285 }
1286 },
1030 "node_modules/@types/sizzle": {1287 "node_modules/@types/sizzle": {
1031 "version": "2.3.8",1288 "version": "2.3.8",
1032 "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",1289 "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
@@ -1050,6 +1307,43 @@
1050 "dev": true,1307 "dev": true,
1051 "license": "MIT"1308 "license": "MIT"
1052 },1309 },
1310 "node_modules/@types/write-file-atomic": {
1311 "version": "4.0.3",
1312 "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
1313 "integrity": "sha512-qdo+vZRchyJIHNeuI1nrpsLw+hnkgqP/8mlaN6Wle/NKhydHmUN9l4p3ZE8yP90AJNJW4uB8HQhedb4f1vNayQ==",
1314 "dev": true,
1315 "license": "MIT",
1316 "dependencies": {
1317 "@types/node": "*"
1318 }
1319 },
1320 "node_modules/@types/yargs": {
1321 "version": "17.0.33",
1322 "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
1323 "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
1324 "dev": true,
1325 "license": "MIT",
1326 "dependencies": {
1327 "@types/yargs-parser": "*"
1328 }
1329 },
1330 "node_modules/@types/yargs-parser": {
1331 "version": "21.0.3",
1332 "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
1333 "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
1334 "dev": true,
1335 "license": "MIT"
1336 },
1337 "node_modules/@types/yauzl": {
1338 "version": "2.10.3",
1339 "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
1340 "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
1341 "dev": true,
1342 "license": "MIT",
1343 "dependencies": {
1344 "@types/node": "*"
1345 }
1346 },
1053 "node_modules/@ungap/structured-clone": {1347 "node_modules/@ungap/structured-clone": {
1054 "version": "1.2.0",1348 "version": "1.2.0",
1055 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",1349 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -3009,6 +3303,29 @@
3009 "pend": "~1.2.0"3303 "pend": "~1.2.0"
3010 }3304 }
3011 },3305 },
3306 "node_modules/fetch-blob": {
3307 "version": "3.2.0",
3308 "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
3309 "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
3310 "funding": [
3311 {
3312 "type": "github",
3313 "url": "https://github.com/sponsors/jimmywarting"
3314 },
3315 {
3316 "type": "paypal",
3317 "url": "https://paypal.me/jimmywarting"
3318 }
3319 ],
3320 "license": "MIT",
3321 "dependencies": {
3322 "node-domexception": "^1.0.0",
3323 "web-streams-polyfill": "^3.0.3"
3324 },
3325 "engines": {
3326 "node": "^12.20 || >= 14.13"
3327 }
3328 },
3012 "node_modules/file-entry-cache": {3329 "node_modules/file-entry-cache": {
3013 "version": "6.0.1",3330 "version": "6.0.1",
3014 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",3331 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3178,6 +3495,18 @@
3178 "node": ">= 14"3495 "node": ">= 14"
3179 }3496 }
3180 },3497 },
3498 "node_modules/formdata-polyfill": {
3499 "version": "4.0.10",
3500 "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
3501 "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
3502 "license": "MIT",
3503 "dependencies": {
3504 "fetch-blob": "^3.1.2"
3505 },
3506 "engines": {
3507 "node": ">=12.20.0"
3508 }
3509 },
3181 "node_modules/forwarded": {3510 "node_modules/forwarded": {
3182 "version": "0.2.0",3511 "version": "0.2.0",
3183 "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",3512 "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -3364,11 +3693,18 @@
3364 "url": "https://github.com/sponsors/sindresorhus"3693 "url": "https://github.com/sponsors/sindresorhus"
3365 }3694 }
3366 },3695 },
3367 "node_modules/google-translate-api-browser": {3696 "node_modules/google-translate-api-x": {
3368 "version": "3.0.1",3697 "version": "10.7.1",
3369 "resolved": "https://registry.npmjs.org/google-translate-api-browser/-/google-translate-api-browser-3.0.1.tgz",3698 "resolved": "https://registry.npmjs.org/google-translate-api-x/-/google-translate-api-x-10.7.1.tgz",
3370 "integrity": "sha512-KTLodkyGBWMK9IW6QIeJ2zCuju4Z0CLpbkADKo+yLhbSTD4l+CXXpQ/xaynGVAzeBezzJG6qn8MLeqOq3SmW0A==",3699 "integrity": "sha512-OdZDS6jRWzn1woOk62aOKQ5OyVaJSA+eyc6CktOWxo36IWfstOjwG/dkvnGl3Z2Sbpmk1A+jc2WwrBiRjqaY2A==",
3371 "license": "MIT"3700 "license": "MIT",
3701 "engines": {
3702 "node": ">=14.0.0"
3703 },
3704 "funding": {
3705 "type": "github",
3706 "url": "https://github.com/sponsors/AidanWelch"
3707 }
3372 },3708 },
3373 "node_modules/gopd": {3709 "node_modules/gopd": {
3374 "version": "1.0.1",3710 "version": "1.0.1",
@@ -3489,14 +3825,6 @@
3489 "node": ">= 0.4"3825 "node": ">= 0.4"
3490 }3826 }
3491 },3827 },
3492 "node_modules/he": {
3493 "version": "1.2.0",
3494 "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
3495 "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
3496 "bin": {
3497 "he": "bin/he"
3498 }
3499 },
3500 "node_modules/helmet": {3828 "node_modules/helmet": {
3501 "version": "7.1.0",3829 "version": "7.1.0",
3502 "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",3830 "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",
@@ -3505,6 +3833,22 @@
3505 "node": ">=16.0.0"3833 "node": ">=16.0.0"
3506 }3834 }
3507 },3835 },
3836 "node_modules/html-entities": {
3837 "version": "2.5.2",
3838 "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
3839 "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
3840 "funding": [
3841 {
3842 "type": "github",
3843 "url": "https://github.com/sponsors/mdevils"
3844 },
3845 {
3846 "type": "patreon",
3847 "url": "https://patreon.com/mdevils"
3848 }
3849 ],
3850 "license": "MIT"
3851 },
3508 "node_modules/htmlparser2": {3852 "node_modules/htmlparser2": {
3509 "version": "8.0.2",3853 "version": "8.0.2",
3510 "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",3854 "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -3691,6 +4035,12 @@
3691 "@types/node": "16.9.1"4035 "@types/node": "16.9.1"
3692 }4036 }
3693 },4037 },
4038 "node_modules/image-q/node_modules/@types/node": {
4039 "version": "16.9.1",
4040 "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
4041 "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
4042 "license": "MIT"
4043 },
3694 "node_modules/import-fresh": {4044 "node_modules/import-fresh": {
3695 "version": "3.3.0",4045 "version": "3.3.0",
3696 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",4046 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3876,6 +4226,26 @@
3876 "whatwg-fetch": "^3.4.1"4226 "whatwg-fetch": "^3.4.1"
3877 }4227 }
3878 },4228 },
4229 "node_modules/isomorphic-fetch/node_modules/node-fetch": {
4230 "version": "2.7.0",
4231 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
4232 "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
4233 "license": "MIT",
4234 "dependencies": {
4235 "whatwg-url": "^5.0.0"
4236 },
4237 "engines": {
4238 "node": "4.x || >=6.0.0"
4239 },
4240 "peerDependencies": {
4241 "encoding": "^0.1.0"
4242 },
4243 "peerDependenciesMeta": {
4244 "encoding": {
4245 "optional": true
4246 }
4247 }
4248 },
3879 "node_modules/jackspeak": {4249 "node_modules/jackspeak": {
3880 "version": "2.3.6",4250 "version": "2.3.6",
3881 "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",4251 "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -4355,23 +4725,30 @@
4355 }4725 }
4356 },4726 },
4357 "node_modules/node-fetch": {4727 "node_modules/node-fetch": {
4358 "version": "2.6.12",4728 "version": "3.3.2",
4359 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",4729 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
4360 "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",4730 "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
4361 "license": "MIT",4731 "license": "MIT",
4362 "dependencies": {4732 "dependencies": {
4363 "whatwg-url": "^5.0.0"4733 "data-uri-to-buffer": "^4.0.0",
4734 "fetch-blob": "^3.1.4",
4735 "formdata-polyfill": "^4.0.10"
4364 },4736 },
4365 "engines": {4737 "engines": {
4366 "node": "4.x || >=6.0.0"4738 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4367 },4739 },
4368 "peerDependencies": {4740 "funding": {
4369 "encoding": "^0.1.0"4741 "type": "opencollective",
4370 },4742 "url": "https://opencollective.com/node-fetch"
4371 "peerDependenciesMeta": {4743 }
4372 "encoding": {4744 },
4373 "optional": true4745 "node_modules/node-fetch/node_modules/data-uri-to-buffer": {
4374 }4746 "version": "4.0.1",
4747 "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
4748 "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
4749 "license": "MIT",
4750 "engines": {
4751 "node": ">= 12"
4375 }4752 }
4376 },4753 },
4377 "node_modules/node-persist": {4754 "node_modules/node-persist": {
@@ -4537,13 +4914,24 @@
4537 "openai": "bin/cli"4914 "openai": "bin/cli"
4538 }4915 }
4539 },4916 },
4540 "node_modules/openai/node_modules/@types/node": {4917 "node_modules/openai/node_modules/node-fetch": {
4541 "version": "18.18.9",4918 "version": "2.7.0",
4542 "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz",4919 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
4543 "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==",4920 "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
4544 "license": "MIT",4921 "license": "MIT",
4545 "dependencies": {4922 "dependencies": {
4546 "undici-types": "~5.26.4"4923 "whatwg-url": "^5.0.0"
4924 },
4925 "engines": {
4926 "node": "4.x || >=6.0.0"
4927 },
4928 "peerDependencies": {
4929 "encoding": "^0.1.0"
4930 },
4931 "peerDependenciesMeta": {
4932 "encoding": {
4933 "optional": true
4934 }
4547 }4935 }
4548 },4936 },
4549 "node_modules/optionator": {4937 "node_modules/optionator": {
@@ -5808,6 +6196,7 @@
5808 "version": "5.0.0",6196 "version": "5.0.0",
5809 "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",6197 "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
5810 "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",6198 "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
6199 "license": "MIT",
5811 "dependencies": {6200 "dependencies": {
5812 "punycode": "^2.3.1"6201 "punycode": "^2.3.1"
5813 },6202 },
@@ -6012,6 +6401,7 @@
6012 "version": "7.0.0",6401 "version": "7.0.0",
6013 "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",6402 "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
6014 "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",6403 "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
6404 "license": "BSD-2-Clause",
6015 "engines": {6405 "engines": {
6016 "node": ">=12"6406 "node": ">=12"
6017 }6407 }
@@ -6026,6 +6416,7 @@
6026 "version": "14.0.0",6416 "version": "14.0.0",
6027 "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",6417 "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
6028 "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",6418 "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
6419 "license": "MIT",
6029 "dependencies": {6420 "dependencies": {
6030 "tr46": "^5.0.0",6421 "tr46": "^5.0.0",
6031 "webidl-conversions": "^7.0.0"6422 "webidl-conversions": "^7.0.0"
package.json+23 -4
@@ -14,9 +14,9 @@
14 "csrf-csrf": "^2.2.3",14 "csrf-csrf": "^2.2.3",
15 "express": "^4.21.0",15 "express": "^4.21.0",
16 "form-data": "^4.0.0",16 "form-data": "^4.0.0",
17 "google-translate-api-browser": "^3.0.1",17 "google-translate-api-x": "^10.7.1",
18 "he": "^1.2.0",
19 "helmet": "^7.1.0",18 "helmet": "^7.1.0",
19 "html-entities": "^2.5.2",
20 "iconv-lite": "^0.6.3",20 "iconv-lite": "^0.6.3",
21 "ip-matching": "^2.1.2",21 "ip-matching": "^2.1.2",
22 "ipaddr.js": "^2.0.1",22 "ipaddr.js": "^2.0.1",
@@ -24,7 +24,7 @@
24 "lodash": "^4.17.21",24 "lodash": "^4.17.21",
25 "mime-types": "^2.1.35",25 "mime-types": "^2.1.35",
26 "multer": "^1.4.5-lts.1",26 "multer": "^1.4.5-lts.1",
27 "node-fetch": "^2.6.11",27 "node-fetch": "^3.3.2",
28 "node-persist": "^4.0.1",28 "node-persist": "^4.0.1",
29 "open": "^8.4.2",29 "open": "^8.4.2",
30 "png-chunk-text": "^1.0.0",30 "png-chunk-text": "^1.0.0",
@@ -60,7 +60,7 @@
60 }60 }
61 },61 },
62 "name": "sillytavern",62 "name": "sillytavern",
63 "type": "commonjs",63 "type": "module",
64 "license": "AGPL-3.0",64 "license": "AGPL-3.0",
65 "repository": {65 "repository": {
66 "type": "git",66 "type": "git",
@@ -85,9 +85,28 @@
85 },85 },
86 "main": "server.js",86 "main": "server.js",
87 "devDependencies": {87 "devDependencies": {
88 "@types/archiver": "^6.0.2",
89 "@types/command-exists": "^1.2.3",
90 "@types/compression": "^1.7.5",
91 "@types/cookie-parser": "^1.4.7",
92 "@types/cookie-session": "^2.0.49",
93 "@types/cors": "^2.8.17",
88 "@types/dompurify": "^3.0.5",94 "@types/dompurify": "^3.0.5",
95 "@types/express": "^4.17.21",
89 "@types/jquery": "^3.5.29",96 "@types/jquery": "^3.5.29",
97 "@types/lodash": "^4.17.10",
98 "@types/mime-types": "^2.1.4",
99 "@types/multer": "^1.4.12",
100 "@types/node": "^18.19.55",
101 "@types/node-persist": "^3.1.8",
102 "@types/png-chunk-text": "^1.0.3",
103 "@types/png-chunks-encode": "^1.0.2",
104 "@types/png-chunks-extract": "^1.0.2",
105 "@types/response-time": "^2.3.8",
90 "@types/toastr": "^2.1.43",106 "@types/toastr": "^2.1.43",
107 "@types/write-file-atomic": "^4.0.3",
108 "@types/yargs": "^17.0.33",
109 "@types/yauzl": "^2.10.3",
91 "eslint": "^8.57.0"110 "eslint": "^8.57.0"
92 }111 }
93}112}
plugins.js+16 -4
@@ -3,16 +3,28 @@
3// 1. node plugins.js update3// 1. node plugins.js update
4// 2. node plugins.js install <plugin-git-url>4// 2. node plugins.js install <plugin-git-url>
5// More operations coming soon.5// More operations coming soon.
6const { default: git } = require('simple-git');6import fs from 'node:fs';
7const fs = require('fs');7import path from 'node:path';
8const path = require('path');8import process from 'node:process';
9const { color } = require('./src/util');9import { fileURLToPath } from 'node:url';
1010
11import { default as git } from 'simple-git';
12import { color } from './src/util.js';
13
14const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
11process.chdir(__dirname);15process.chdir(__dirname);
12const pluginsPath = './plugins';16const pluginsPath = './plugins';
1317
14const command = process.argv[2];18const command = process.argv[2];
1519
20if (!command) {
21 console.log('Usage: node plugins.js <command>');
22 console.log('Commands:');
23 console.log(' update - Update all installed plugins');
24 console.log(' install <plugin-git-url> - Install plugin from a Git URL');
25 process.exit(1);
26}
27
16if (command === 'update') {28if (command === 'update') {
17 console.log(color.magenta('Updating all plugins'));29 console.log(color.magenta('Updating all plugins'));
18 updatePlugins();30 updatePlugins();
plugins/package.json+4 -0
@@ -0,0 +1,4 @@
1{
2 "name": "sillytavern-plugins",
3 "type": "commonjs"
4}
post-install.js+15 -11
@@ -1,11 +1,13 @@
1/**1/**
2 * Scripts to be done before starting the server for the first time.2 * Scripts to be done before starting the server for the first time.
3 */3 */
4const fs = require('fs');4import fs from 'node:fs';
5const path = require('path');5import path from 'node:path';
6const crypto = require('crypto');6import crypto from 'node:crypto';
7const yaml = require('yaml');7import process from 'node:process';
8const _ = require('lodash');8import yaml from 'yaml';
9import _ from 'lodash';
10import { createRequire } from 'node:module';
911
10/**12/**
11 * Colorizes console output.13 * Colorizes console output.
@@ -59,13 +61,15 @@ function convertConfig() {
5961
60 try {62 try {
61 console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));63 console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));
62 const config = require(path.join(process.cwd(), './config.conf'));64 fs.renameSync('./config.conf', './config.conf.cjs'); // Force loading as CommonJS
63 fs.copyFileSync('./config.conf', './config.conf.bak');65 const require = createRequire(import.meta.url);
64 fs.rmSync('./config.conf');66 const config = require(path.join(process.cwd(), './config.conf.cjs'));
67 fs.copyFileSync('./config.conf.cjs', './config.conf.bak');
68 fs.rmSync('./config.conf.cjs');
65 fs.writeFileSync('./config.yaml', yaml.stringify(config));69 fs.writeFileSync('./config.yaml', yaml.stringify(config));
66 console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.'));70 console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.'));
67 } catch (error) {71 } catch (error) {
68 console.error(color.red('FATAL: Config conversion failed. Please check your config.conf file and try again.'));72 console.error(color.red('FATAL: Config conversion failed. Please check your config.conf file and try again.'), error);
69 return;73 return;
70 }74 }
71 }75 }
@@ -75,7 +79,7 @@ function convertConfig() {
75 * Compares the current config.yaml with the default config.yaml and adds any missing values.79 * Compares the current config.yaml with the default config.yaml and adds any missing values.
76 */80 */
77function addMissingConfigValues() {81function addMissingConfigValues() {
78 try {82 try {
79 const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8'));83 const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8'));
80 let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));84 let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));
8185
@@ -132,7 +136,7 @@ function createDefaultFiles() {
132function getMd5Hash(data) {136function getMd5Hash(data) {
133 return crypto137 return crypto
134 .createHash('md5')138 .createHash('md5')
135 .update(data)139 .update(new Uint8Array(data))
136 .digest('hex');140 .digest('hex');
137}141}
138142
recover.js+16 -10
@@ -1,7 +1,13 @@
1const yaml = require('yaml');1import fs from 'node:fs';
2const fs = require('fs');2import process from 'node:process';
3const storage = require('node-persist');3import yaml from 'yaml';
4const users = require('./src/users');4import storage from 'node-persist';
5import {
6 initUserStorage,
7 getPasswordSalt,
8 getPasswordHash,
9 toKey,
10} from './src/users.js';
511
6const userAccount = process.argv[2];12const userAccount = process.argv[2];
7const userPassword = process.argv[3];13const userPassword = process.argv[3];
@@ -22,7 +28,7 @@ async function initStorage() {
22 process.exit(1);28 process.exit(1);
23 }29 }
2430
25 await users.initUserStorage(dataRoot);31 await initUserStorage(dataRoot);
26}32}
2733
28async function main() {34async function main() {
@@ -31,22 +37,22 @@ async function main() {
31 /**37 /**
32 * @type {import('./src/users').User}38 * @type {import('./src/users').User}
33 */39 */
34 const user = await storage.get(users.toKey(userAccount));40 const user = await storage.get(toKey(userAccount));
3541
36 if (!user) {42 if (!user) {
37 console.error(`User "${userAccount}" not found.`);43 console.error(`User "${userAccount}" not found.`);
38 process.exit(1);44 process.exit(1);
39 }45 }
4046
41 if (!user.enabled) {47 if (!user.enabled) {
42 console.log('User is disabled. Enabling...');48 console.log('User is disabled. Enabling...');
43 user.enabled = true;49 user.enabled = true;
44 }50 }
4551
46 if (userPassword) {52 if (userPassword) {
47 console.log('Setting new password...');53 console.log('Setting new password...');
48 const salt = users.getPasswordSalt();54 const salt = getPasswordSalt();
49 const passwordHash = users.getPasswordHash(userPassword, salt);55 const passwordHash = getPasswordHash(userPassword, salt);
50 user.password = passwordHash;56 user.password = passwordHash;
51 user.salt = salt;57 user.salt = salt;
52 } else {58 } else {
@@ -55,7 +61,7 @@ async function main() {
55 user.salt = '';61 user.salt = '';
56 }62 }
5763
58 await storage.setItem(users.toKey(userAccount), user);64 await storage.setItem(toKey(userAccount), user);
59 console.log('User recovered. A program will exit now.');65 console.log('User recovered. A program will exit now.');
60}66}
6167
server.js+162 -182
@@ -1,32 +1,35 @@
1#!/usr/bin/env node1#!/usr/bin/env node
22
3// native node modules3// native node modules
4const fs = require('fs');4import fs from 'node:fs';
5const http = require('http');5import http from 'node:http';
6const https = require('https');6import https from 'node:https';
7const path = require('path');7import path from 'node:path';
8const util = require('util');8import util from 'node:util';
9import net from 'node:net';
10import dns from 'node:dns';
11import process from 'node:process';
12import { fileURLToPath } from 'node:url';
913
10// cli/fs related library imports14// cli/fs related library imports
11const open = require('open');15import open from 'open';
12const yargs = require('yargs/yargs');16import yargs from 'yargs/yargs';
13const { hideBin } = require('yargs/helpers');17import { hideBin } from 'yargs/helpers';
1418
15// express/server related library imports19// express/server related library imports
16const cors = require('cors');20import cors from 'cors';
17const doubleCsrf = require('csrf-csrf').doubleCsrf;21import { doubleCsrf } from 'csrf-csrf';
18const express = require('express');22import express from 'express';
19const compression = require('compression');23import compression from 'compression';
20const cookieParser = require('cookie-parser');24import cookieParser from 'cookie-parser';
21const cookieSession = require('cookie-session');25import cookieSession from 'cookie-session';
22const multer = require('multer');26import multer from 'multer';
23const responseTime = require('response-time');27import responseTime from 'response-time';
24const helmet = require('helmet').default;28import helmet from 'helmet';
29import bodyParser from 'body-parser';
2530
26// net related library imports31// net related library imports
27const net = require('net');32import fetch from 'node-fetch';
28const dns = require('dns');
29const fetch = require('node-fetch').default;
3033
31// Unrestrict console logs display limit34// Unrestrict console logs display limit
32util.inspect.defaultOptions.maxArrayLength = null;35util.inspect.defaultOptions.maxArrayLength = null;
@@ -34,20 +37,81 @@ util.inspect.defaultOptions.maxStringLength = null;
34util.inspect.defaultOptions.depth = 4;37util.inspect.defaultOptions.depth = 4;
3538
36// local library imports39// local library imports
37const userModule = require('./src/users');40import{ loadPlugins } from './src/plugin-loader.js';
38const basicAuthMiddleware = require('./src/middleware/basicAuth');41import {
39const whitelistMiddleware = require('./src/middleware/whitelist');42 initUserStorage,
40const initRequestProxy = require('./src/request-proxy');43 getCsrfSecret,
41const contentManager = require('./src/endpoints/content-manager');44 getCookieSecret,
42const {45 getCookieSessionName,
46 getAllEnabledUsers,
47 ensurePublicDirectoriesExist,
48 getUserDirectoriesList,
49 migrateSystemPrompts,
50 migrateUserData,
51 requireLoginMiddleware,
52 setUserDataMiddleware,
53 shouldRedirectToLogin,
54 tryAutoLogin,
55 router as userDataRouter,
56} from './src/users.js';
57import basicAuthMiddleware from './src/middleware/basicAuth.js';
58import whitelistMiddleware from './src/middleware/whitelist.js';
59import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
60import initRequestProxy from './src/request-proxy.js';
61import {
43 getVersion,62 getVersion,
44 getConfigValue,63 getConfigValue,
45 color,64 color,
46 forwardFetchResponse,65 forwardFetchResponse,
47 removeColorFormatting,66 removeColorFormatting,
48 getSeparator,67 getSeparator,
49} = require('./src/util');68} from './src/util.js';
50const { ensureThumbnailCache } = require('./src/endpoints/thumbnails');69import { UPLOADS_DIRECTORY } from './src/constants.js';
70import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
71
72// Routers
73import { router as usersPublicRouter } from './src/endpoints/users-public.js';
74import { router as usersPrivateRouter } from './src/endpoints/users-private.js';
75import { router as usersAdminRouter } from './src/endpoints/users-admin.js';
76import { router as movingUIRouter } from './src/endpoints/moving-ui.js';
77import { router as imagesRouter } from './src/endpoints/images.js';
78import { router as quickRepliesRouter } from './src/endpoints/quick-replies.js';
79import { router as avatarsRouter } from './src/endpoints/avatars.js';
80import { router as themesRouter } from './src/endpoints/themes.js';
81import { router as openAiRouter } from './src/endpoints/openai.js';
82import { router as googleRouter } from './src/endpoints/google.js';
83import { router as anthropicRouter } from './src/endpoints/anthropic.js';
84import { router as tokenizersRouter } from './src/endpoints/tokenizers.js';
85import { router as presetsRouter } from './src/endpoints/presets.js';
86import { router as secretsRouter } from './src/endpoints/secrets.js';
87import { router as thumbnailRouter } from './src/endpoints/thumbnails.js';
88import { router as novelAiRouter } from './src/endpoints/novelai.js';
89import { router as extensionsRouter } from './src/endpoints/extensions.js';
90import { router as assetsRouter } from './src/endpoints/assets.js';
91import { router as filesRouter } from './src/endpoints/files.js';
92import { router as charactersRouter } from './src/endpoints/characters.js';
93import { router as chatsRouter } from './src/endpoints/chats.js';
94import { router as groupsRouter } from './src/endpoints/groups.js';
95import { router as worldInfoRouter } from './src/endpoints/worldinfo.js';
96import { router as statsRouter, init as statsInit, onExit as statsOnExit } from './src/endpoints/stats.js';
97import { router as backgroundsRouter } from './src/endpoints/backgrounds.js';
98import { router as spritesRouter } from './src/endpoints/sprites.js';
99import { router as contentManagerRouter, checkForNewContent } from './src/endpoints/content-manager.js';
100import { router as settingsRouter, init as settingsInit } from './src/endpoints/settings.js';
101import { router as stableDiffusionRouter } from './src/endpoints/stable-diffusion.js';
102import { router as hordeRouter } from './src/endpoints/horde.js';
103import { router as vectorsRouter } from './src/endpoints/vectors.js';
104import { router as translateRouter } from './src/endpoints/translate.js';
105import { router as classifyRouter } from './src/endpoints/classify.js';
106import { router as captionRouter } from './src/endpoints/caption.js';
107import { router as searchRouter } from './src/endpoints/search.js';
108import { router as openRouterRouter } from './src/endpoints/openrouter.js';
109import { router as chatCompletionsRouter } from './src/endpoints/backends/chat-completions.js';
110import { router as koboldRouter } from './src/endpoints/backends/kobold.js';
111import { router as textCompletionsRouter } from './src/endpoints/backends/text-completions.js';
112import { router as scaleAltRouter } from './src/endpoints/backends/scale-alt.js';
113import { router as speechRouter } from './src/endpoints/speech.js';
114import { router as azureRouter } from './src/endpoints/azure.js';
51115
52// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.116// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
53// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870117// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
@@ -166,8 +230,8 @@ const cliArguments = yargs(hideBin(process.argv))
166 }).parseSync();230 }).parseSync();
167231
168// change all relative paths232// change all relative paths
169console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment.`);233const serverDirectory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
170const serverDirectory = __dirname;234console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
171process.chdir(serverDirectory);235process.chdir(serverDirectory);
172236
173const app = express();237const app = express();
@@ -188,7 +252,7 @@ const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMod
188const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);252const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
189const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);253const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
190254
191const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);255const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY);
192256
193const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);257const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
194const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);258const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
@@ -232,7 +296,6 @@ if (listen && basicAuthMode) app.use(basicAuthMiddleware);
232app.use(whitelistMiddleware(enableWhitelist, listen));296app.use(whitelistMiddleware(enableWhitelist, listen));
233297
234if (enableCorsProxy) {298if (enableCorsProxy) {
235 const bodyParser = require('body-parser');
236 app.use(bodyParser.json({299 app.use(bodyParser.json({
237 limit: '200mb',300 limit: '200mb',
238 }));301 }));
@@ -301,24 +364,23 @@ function getSessionCookieAge() {
301}364}
302365
303app.use(cookieSession({366app.use(cookieSession({
304 name: userModule.getCookieSessionName(),367 name: getCookieSessionName(),
305 sameSite: 'strict',368 sameSite: 'strict',
306 httpOnly: true,369 httpOnly: true,
307 maxAge: getSessionCookieAge(),370 maxAge: getSessionCookieAge(),
308 secret: userModule.getCookieSecret(),371 secret: getCookieSecret(),
309}));372}));
310373
311app.use(userModule.setUserDataMiddleware);374app.use(setUserDataMiddleware);
312375
313// CSRF Protection //376// CSRF Protection //
314if (!disableCsrf) {377if (!disableCsrf) {
315 const COOKIES_SECRET = userModule.getCookieSecret();378 const COOKIES_SECRET = getCookieSecret();
316379
317 const { generateToken, doubleCsrfProtection } = doubleCsrf({380 const { generateToken, doubleCsrfProtection } = doubleCsrf({
318 getSecret: userModule.getCsrfSecret,381 getSecret: getCsrfSecret,
319 cookieName: 'X-CSRF-Token',382 cookieName: 'X-CSRF-Token',
320 cookieOptions: {383 cookieOptions: {
321 httpOnly: true,
322 sameSite: 'strict',384 sameSite: 'strict',
323 secure: false,385 secure: false,
324 },386 },
@@ -346,7 +408,7 @@ if (!disableCsrf) {
346// Static files408// Static files
347// Host index page409// Host index page
348app.get('/', (request, response) => {410app.get('/', (request, response) => {
349 if (userModule.shouldRedirectToLogin(request)) {411 if (shouldRedirectToLogin(request)) {
350 const query = request.url.split('?')[1];412 const query = request.url.split('?')[1];
351 const redirectUrl = query ? `/login?${query}` : '/login';413 const redirectUrl = query ? `/login?${query}` : '/login';
352 return response.redirect(redirectUrl);414 return response.redirect(redirectUrl);
@@ -363,7 +425,7 @@ app.get('/login', async (request, response) => {
363 }425 }
364426
365 try {427 try {
366 const autoLogin = await userModule.tryAutoLogin(request, basicAuthMode);428 const autoLogin = await tryAutoLogin(request, basicAuthMode);
367429
368 if (autoLogin) {430 if (autoLogin) {
369 return response.redirect('/');431 return response.redirect('/');
@@ -379,22 +441,22 @@ app.get('/login', async (request, response) => {
379app.use(express.static(process.cwd() + '/public', {}));441app.use(express.static(process.cwd() + '/public', {}));
380442
381// Public API443// Public API
382app.use('/api/users', require('./src/endpoints/users-public').router);444app.use('/api/users', usersPublicRouter);
383445
384// Everything below this line requires authentication446// Everything below this line requires authentication
385app.use(userModule.requireLoginMiddleware);447app.use(requireLoginMiddleware);
386app.get('/api/ping', (_, response) => response.sendStatus(204));448app.get('/api/ping', (_, response) => response.sendStatus(204));
387449
388// File uploads450// File uploads
389app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));451app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
390app.use(require('./src/middleware/multerMonkeyPatch'));452app.use(multerMonkeyPatch);
391453
392// User data mount454// User data mount
393app.use('/', userModule.router);455app.use('/', userDataRouter);
394// Private endpoints456// Private endpoints
395app.use('/api/users', require('./src/endpoints/users-private').router);457app.use('/api/users', usersPrivateRouter);
396// Admin endpoints458// Admin endpoints
397app.use('/api/users', require('./src/endpoints/users-admin').router);459app.use('/api/users', usersAdminRouter);
398460
399app.get('/version', async function (_, response) {461app.get('/version', async function (_, response) {
400 const data = await getVersion();462 const data = await getVersion();
@@ -509,126 +571,45 @@ redirect('/api/serpapi/search', '/api/search/serpapi');
509redirect('/api/serpapi/visit', '/api/search/visit');571redirect('/api/serpapi/visit', '/api/search/visit');
510redirect('/api/serpapi/transcript', '/api/search/transcript');572redirect('/api/serpapi/transcript', '/api/search/transcript');
511573
512// Moving UI574app.use('/api/moving-ui', movingUIRouter);
513app.use('/api/moving-ui', require('./src/endpoints/moving-ui').router);575app.use('/api/images', imagesRouter);
514576app.use('/api/quick-replies', quickRepliesRouter);
515// Image management577app.use('/api/avatars', avatarsRouter);
516app.use('/api/images', require('./src/endpoints/images').router);578app.use('/api/themes', themesRouter);
517579app.use('/api/openai', openAiRouter);
518// Quick reply management580app.use('/api/google', googleRouter);
519app.use('/api/quick-replies', require('./src/endpoints/quick-replies').router);581app.use('/api/anthropic', anthropicRouter);
520582app.use('/api/tokenizers', tokenizersRouter);
521// Avatar management583app.use('/api/presets', presetsRouter);
522app.use('/api/avatars', require('./src/endpoints/avatars').router);584app.use('/api/secrets', secretsRouter);
523585app.use('/thumbnail', thumbnailRouter);
524// Theme management586app.use('/api/novelai', novelAiRouter);
525app.use('/api/themes', require('./src/endpoints/themes').router);587app.use('/api/extensions', extensionsRouter);
526588app.use('/api/assets', assetsRouter);
527// OpenAI API589app.use('/api/files', filesRouter);
528app.use('/api/openai', require('./src/endpoints/openai').router);590app.use('/api/characters', charactersRouter);
529591app.use('/api/chats', chatsRouter);
530//Google API592app.use('/api/groups', groupsRouter);
531app.use('/api/google', require('./src/endpoints/google').router);593app.use('/api/worldinfo', worldInfoRouter);
532594app.use('/api/stats', statsRouter);
533//Anthropic API595app.use('/api/backgrounds', backgroundsRouter);
534app.use('/api/anthropic', require('./src/endpoints/anthropic').router);596app.use('/api/sprites', spritesRouter);
535597app.use('/api/content', contentManagerRouter);
536// Tokenizers598app.use('/api/settings', settingsRouter);
537app.use('/api/tokenizers', require('./src/endpoints/tokenizers').router);599app.use('/api/sd', stableDiffusionRouter);
538600app.use('/api/horde', hordeRouter);
539// Preset management601app.use('/api/vector', vectorsRouter);
540app.use('/api/presets', require('./src/endpoints/presets').router);602app.use('/api/translate', translateRouter);
541603app.use('/api/extra/classify', classifyRouter);
542// Secrets managemenet604app.use('/api/extra/caption', captionRouter);
543app.use('/api/secrets', require('./src/endpoints/secrets').router);605app.use('/api/search', searchRouter);
544606app.use('/api/backends/text-completions', textCompletionsRouter);
545// Thumbnail generation. These URLs are saved in chat, so this route cannot be renamed!607app.use('/api/openrouter', openRouterRouter);
546app.use('/thumbnail', require('./src/endpoints/thumbnails').router);608app.use('/api/backends/kobold', koboldRouter);
547609app.use('/api/backends/chat-completions', chatCompletionsRouter);
548// NovelAI generation610app.use('/api/backends/scale-alt', scaleAltRouter);
549app.use('/api/novelai', require('./src/endpoints/novelai').router);611app.use('/api/speech', speechRouter);
550612app.use('/api/azure', azureRouter);
551// Third-party extensions
552app.use('/api/extensions', require('./src/endpoints/extensions').router);
553
554// Asset management
555app.use('/api/assets', require('./src/endpoints/assets').router);
556
557// File management
558app.use('/api/files', require('./src/endpoints/files').router);
559
560// Character management
561app.use('/api/characters', require('./src/endpoints/characters').router);
562
563// Chat management
564app.use('/api/chats', require('./src/endpoints/chats').router);
565
566// Group management
567app.use('/api/groups', require('./src/endpoints/groups').router);
568
569// World info management
570app.use('/api/worldinfo', require('./src/endpoints/worldinfo').router);
571
572// Stats calculation
573const statsEndpoint = require('./src/endpoints/stats');
574app.use('/api/stats', statsEndpoint.router);
575
576// Background management
577app.use('/api/backgrounds', require('./src/endpoints/backgrounds').router);
578
579// Character sprite management
580app.use('/api/sprites', require('./src/endpoints/sprites').router);
581
582// Custom content management
583app.use('/api/content', require('./src/endpoints/content-manager').router);
584
585// Settings load/store
586const settingsEndpoint = require('./src/endpoints/settings');
587app.use('/api/settings', settingsEndpoint.router);
588
589// Stable Diffusion generation
590app.use('/api/sd', require('./src/endpoints/stable-diffusion').router);
591
592// LLM and SD Horde generation
593app.use('/api/horde', require('./src/endpoints/horde').router);
594
595// Vector storage DB
596app.use('/api/vector', require('./src/endpoints/vectors').router);
597
598// Chat translation
599app.use('/api/translate', require('./src/endpoints/translate').router);
600
601// Emotion classification
602app.use('/api/extra/classify', require('./src/endpoints/classify').router);
603
604// Image captioning
605app.use('/api/extra/caption', require('./src/endpoints/caption').router);
606
607// Web search and scraping
608app.use('/api/search', require('./src/endpoints/search').router);
609
610// The different text generation APIs
611
612// Ooba/OpenAI text completions
613app.use('/api/backends/text-completions', require('./src/endpoints/backends/text-completions').router);
614
615// OpenRouter
616app.use('/api/openrouter', require('./src/endpoints/openrouter').router);
617
618// KoboldAI
619app.use('/api/backends/kobold', require('./src/endpoints/backends/kobold').router);
620
621// OpenAI chat completions
622app.use('/api/backends/chat-completions', require('./src/endpoints/backends/chat-completions').router);
623
624// Scale (alt method)
625app.use('/api/backends/scale-alt', require('./src/endpoints/backends/scale-alt').router);
626
627// Speech (text-to-speech and speech-to-text)
628app.use('/api/speech', require('./src/endpoints/speech').router);
629
630// Azure TTS
631app.use('/api/azure', require('./src/endpoints/azure').router);
632613
633const tavernUrlV6 = new URL(614const tavernUrlV6 = new URL(
634 (cliArguments.ssl ? 'https://' : 'http://') +615 (cliArguments.ssl ? 'https://' : 'http://') +
@@ -658,22 +639,22 @@ const preSetupTasks = async function () {
658 }639 }
659 console.log();640 console.log();
660641
661 const directories = await userModule.getUserDirectoriesList();642 const directories = await getUserDirectoriesList();
662 await contentManager.checkForNewContent(directories);643 await checkForNewContent(directories);
663 await ensureThumbnailCache();644 await ensureThumbnailCache();
664 cleanUploads();645 cleanUploads();
665646
666 await settingsEndpoint.init();647 await settingsInit();
667 await statsEndpoint.init();648 await statsInit();
668649
669 const cleanupPlugins = await loadPlugins();650 const cleanupPlugins = await initializePlugins();
670 const consoleTitle = process.title;651 const consoleTitle = process.title;
671652
672 let isExiting = false;653 let isExiting = false;
673 const exitProcess = async () => {654 const exitProcess = async () => {
674 if (isExiting) return;655 if (isExiting) return;
675 isExiting = true;656 isExiting = true;
676 statsEndpoint.onExit();657 await statsOnExit();
677 if (typeof cleanupPlugins === 'function') {658 if (typeof cleanupPlugins === 'function') {
678 await cleanupPlugins();659 await cleanupPlugins();
679 }660 }
@@ -773,11 +754,10 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
773 * Loads server plugins from a directory.754 * Loads server plugins from a directory.
774 * @returns {Promise<Function>} Function to be run on server exit755 * @returns {Promise<Function>} Function to be run on server exit
775 */756 */
776async function loadPlugins() {757async function initializePlugins() {
777 try {758 try {
778 const pluginDirectory = path.join(serverDirectory, 'plugins');759 const pluginDirectory = path.join(serverDirectory, 'plugins');
779 const loader = require('./src/plugin-loader');760 const cleanupPlugins = await loadPlugins(app, pluginDirectory);
780 const cleanupPlugins = await loader.loadPlugins(app, pluginDirectory);
781 return cleanupPlugins;761 return cleanupPlugins;
782 } catch {762 } catch {
783 console.log('Plugin loading failed.');763 console.log('Plugin loading failed.');
@@ -850,7 +830,7 @@ function createHttpsServer(url) {
850 }, app);830 }, app);
851 server.on('error', reject);831 server.on('error', reject);
852 server.on('listening', resolve);832 server.on('listening', resolve);
853 server.listen(url.port || 443, url.hostname);833 server.listen(Number(url.port || 443), url.hostname);
854 });834 });
855}835}
856836
@@ -865,7 +845,7 @@ function createHttpServer(url) {
865 const server = http.createServer(app);845 const server = http.createServer(app);
866 server.on('error', reject);846 server.on('error', reject);
867 server.on('listening', resolve);847 server.on('listening', resolve);
868 server.listen(url.port || 80, url.hostname);848 server.listen(Number(url.port || 80), url.hostname);
869 });849 });
870}850}
871851
@@ -917,7 +897,7 @@ async function verifySecuritySettings() {
917 logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');897 logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');
918 }898 }
919899
920 const users = await userModule.getAllEnabledUsers();900 const users = await getAllEnabledUsers();
921 const unprotectedUsers = users.filter(x => !x.password);901 const unprotectedUsers = users.filter(x => !x.password);
922 const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin);902 const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin);
923903
@@ -935,10 +915,10 @@ async function verifySecuritySettings() {
935}915}
936916
937// User storage module needs to be initialized before starting the server917// User storage module needs to be initialized before starting the server
938userModule.initUserStorage(dataRoot)918initUserStorage(dataRoot)
939 .then(userModule.ensurePublicDirectoriesExist)919 .then(ensurePublicDirectoriesExist)
940 .then(userModule.migrateUserData)920 .then(migrateUserData)
941 .then(userModule.migrateSystemPrompts)921 .then(migrateSystemPrompts)
942 .then(verifySecuritySettings)922 .then(verifySecuritySettings)
943 .then(preSetupTasks)923 .then(preSetupTasks)
944 .finally(startServer);924 .finally(startServer);
src/additional-headers.js+20 -26
@@ -1,10 +1,10 @@
1const { TEXTGEN_TYPES, OPENROUTER_HEADERS, FEATHERLESS_HEADERS } = require('./constants');1import { TEXTGEN_TYPES, OPENROUTER_HEADERS, FEATHERLESS_HEADERS } from './constants.js';
2const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');2import { SECRET_KEYS, readSecret } from './endpoints/secrets.js';
3const { getConfigValue } = require('./util');3import { getConfigValue } from './util.js';
44
5/**5/**
6 * Gets the headers for the Mancer API.6 * Gets the headers for the Mancer API.
7 * @param {import('./users').UserDirectoryList} directories User directories7 * @param {import('./users.js').UserDirectoryList} directories User directories
8 * @returns {object} Headers for the request8 * @returns {object} Headers for the request
9 */9 */
10function getMancerHeaders(directories) {10function getMancerHeaders(directories) {
@@ -18,7 +18,7 @@ function getMancerHeaders(directories) {
1818
19/**19/**
20 * Gets the headers for the TogetherAI API.20 * Gets the headers for the TogetherAI API.
21 * @param {import('./users').UserDirectoryList} directories User directories21 * @param {import('./users.js').UserDirectoryList} directories User directories
22 * @returns {object} Headers for the request22 * @returns {object} Headers for the request
23 */23 */
24function getTogetherAIHeaders(directories) {24function getTogetherAIHeaders(directories) {
@@ -31,7 +31,7 @@ function getTogetherAIHeaders(directories) {
3131
32/**32/**
33 * Gets the headers for the InfermaticAI API.33 * Gets the headers for the InfermaticAI API.
34 * @param {import('./users').UserDirectoryList} directories User directories34 * @param {import('./users.js').UserDirectoryList} directories User directories
35 * @returns {object} Headers for the request35 * @returns {object} Headers for the request
36 */36 */
37function getInfermaticAIHeaders(directories) {37function getInfermaticAIHeaders(directories) {
@@ -44,7 +44,7 @@ function getInfermaticAIHeaders(directories) {
4444
45/**45/**
46 * Gets the headers for the DreamGen API.46 * Gets the headers for the DreamGen API.
47 * @param {import('./users').UserDirectoryList} directories User directories47 * @param {import('./users.js').UserDirectoryList} directories User directories
48 * @returns {object} Headers for the request48 * @returns {object} Headers for the request
49 */49 */
50function getDreamGenHeaders(directories) {50function getDreamGenHeaders(directories) {
@@ -57,7 +57,7 @@ function getDreamGenHeaders(directories) {
5757
58/**58/**
59 * Gets the headers for the OpenRouter API.59 * Gets the headers for the OpenRouter API.
60 * @param {import('./users').UserDirectoryList} directories User directories60 * @param {import('./users.js').UserDirectoryList} directories User directories
61 * @returns {object} Headers for the request61 * @returns {object} Headers for the request
62 */62 */
63function getOpenRouterHeaders(directories) {63function getOpenRouterHeaders(directories) {
@@ -69,7 +69,7 @@ function getOpenRouterHeaders(directories) {
6969
70/**70/**
71 * Gets the headers for the vLLM API.71 * Gets the headers for the vLLM API.
72 * @param {import('./users').UserDirectoryList} directories User directories72 * @param {import('./users.js').UserDirectoryList} directories User directories
73 * @returns {object} Headers for the request73 * @returns {object} Headers for the request
74 */74 */
75function getVllmHeaders(directories) {75function getVllmHeaders(directories) {
@@ -82,7 +82,7 @@ function getVllmHeaders(directories) {
8282
83/**83/**
84 * Gets the headers for the Aphrodite API.84 * Gets the headers for the Aphrodite API.
85 * @param {import('./users').UserDirectoryList} directories User directories85 * @param {import('./users.js').UserDirectoryList} directories User directories
86 * @returns {object} Headers for the request86 * @returns {object} Headers for the request
87 */87 */
88function getAphroditeHeaders(directories) {88function getAphroditeHeaders(directories) {
@@ -96,7 +96,7 @@ function getAphroditeHeaders(directories) {
9696
97/**97/**
98 * Gets the headers for the Tabby API.98 * Gets the headers for the Tabby API.
99 * @param {import('./users').UserDirectoryList} directories User directories99 * @param {import('./users.js').UserDirectoryList} directories User directories
100 * @returns {object} Headers for the request100 * @returns {object} Headers for the request
101 */101 */
102function getTabbyHeaders(directories) {102function getTabbyHeaders(directories) {
@@ -110,7 +110,7 @@ function getTabbyHeaders(directories) {
110110
111/**111/**
112 * Gets the headers for the LlamaCPP API.112 * Gets the headers for the LlamaCPP API.
113 * @param {import('./users').UserDirectoryList} directories User directories113 * @param {import('./users.js').UserDirectoryList} directories User directories
114 * @returns {object} Headers for the request114 * @returns {object} Headers for the request
115 */115 */
116function getLlamaCppHeaders(directories) {116function getLlamaCppHeaders(directories) {
@@ -123,7 +123,7 @@ function getLlamaCppHeaders(directories) {
123123
124/**124/**
125 * Gets the headers for the Ooba API.125 * Gets the headers for the Ooba API.
126 * @param {import('./users').UserDirectoryList} directories126 * @param {import('./users.js').UserDirectoryList} directories
127 * @returns {object} Headers for the request127 * @returns {object} Headers for the request
128 */128 */
129function getOobaHeaders(directories) {129function getOobaHeaders(directories) {
@@ -136,7 +136,7 @@ function getOobaHeaders(directories) {
136136
137/**137/**
138 * Gets the headers for the KoboldCpp API.138 * Gets the headers for the KoboldCpp API.
139 * @param {import('./users').UserDirectoryList} directories139 * @param {import('./users.js').UserDirectoryList} directories
140 * @returns {object} Headers for the request140 * @returns {object} Headers for the request
141 */141 */
142function getKoboldCppHeaders(directories) {142function getKoboldCppHeaders(directories) {
@@ -149,7 +149,7 @@ function getKoboldCppHeaders(directories) {
149149
150/**150/**
151 * Gets the headers for the Featherless API.151 * Gets the headers for the Featherless API.
152 * @param {import('./users').UserDirectoryList} directories152 * @param {import('./users.js').UserDirectoryList} directories
153 * @returns {object} Headers for the request153 * @returns {object} Headers for the request
154 */154 */
155function getFeatherlessHeaders(directories) {155function getFeatherlessHeaders(directories) {
@@ -161,7 +161,7 @@ function getFeatherlessHeaders(directories) {
161161
162/**162/**
163 * Gets the headers for the HuggingFace API.163 * Gets the headers for the HuggingFace API.
164 * @param {import('./users').UserDirectoryList} directories164 * @param {import('./users.js').UserDirectoryList} directories
165 * @returns {object} Headers for the request165 * @returns {object} Headers for the request
166 */166 */
167function getHuggingFaceHeaders(directories) {167function getHuggingFaceHeaders(directories) {
@@ -172,7 +172,7 @@ function getHuggingFaceHeaders(directories) {
172 }) : {};172 }) : {};
173}173}
174174
175function getOverrideHeaders(urlHost) {175export function getOverrideHeaders(urlHost) {
176 const requestOverrides = getConfigValue('requestOverrides', []);176 const requestOverrides = getConfigValue('requestOverrides', []);
177 const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers;177 const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers;
178 if (overrideHeaders && urlHost) {178 if (overrideHeaders && urlHost) {
@@ -188,7 +188,7 @@ function getOverrideHeaders(urlHost) {
188 * @param {object} args New request arguments188 * @param {object} args New request arguments
189 * @param {string|null} server API server for new request189 * @param {string|null} server API server for new request
190 */190 */
191function setAdditionalHeaders(request, args, server) {191export function setAdditionalHeaders(request, args, server) {
192 setAdditionalHeadersByType(args.headers, request.body.api_type, server, request.user.directories);192 setAdditionalHeadersByType(args.headers, request.body.api_type, server, request.user.directories);
193}193}
194194
@@ -197,9 +197,9 @@ function setAdditionalHeaders(request, args, server) {
197 * @param {object} requestHeaders Request headers197 * @param {object} requestHeaders Request headers
198 * @param {string} type API type198 * @param {string} type API type
199 * @param {string|null} server API server for new request199 * @param {string|null} server API server for new request
200 * @param {import('./users').UserDirectoryList} directories User directories200 * @param {import('./users.js').UserDirectoryList} directories User directories
201 */201 */
202function setAdditionalHeadersByType(requestHeaders, type, server, directories) {202export function setAdditionalHeadersByType(requestHeaders, type, server, directories) {
203 const headerGetters = {203 const headerGetters = {
204 [TEXTGEN_TYPES.MANCER]: getMancerHeaders,204 [TEXTGEN_TYPES.MANCER]: getMancerHeaders,
205 [TEXTGEN_TYPES.VLLM]: getVllmHeaders,205 [TEXTGEN_TYPES.VLLM]: getVllmHeaders,
@@ -234,9 +234,3 @@ function setAdditionalHeadersByType(requestHeaders, type, server, directories) {
234234
235 Object.assign(requestHeaders, headers);235 Object.assign(requestHeaders, headers);
236}236}
237
238module.exports = {
239 getOverrideHeaders,
240 setAdditionalHeaders,
241 setAdditionalHeadersByType,
242};
src/character-card-parser.js+13 -15
@@ -1,8 +1,9 @@
1const fs = require('fs');1import fs from 'node:fs';
2import { Buffer } from 'node:buffer';
23
3const encode = require('png-chunks-encode');4import encode from 'png-chunks-encode';
4const extract = require('png-chunks-extract');5import extract from 'png-chunks-extract';
5const PNGtext = require('png-chunk-text');6import PNGtext from 'png-chunk-text';
67
7/**8/**
8 * Writes Character metadata to a PNG image buffer.9 * Writes Character metadata to a PNG image buffer.
@@ -11,8 +12,8 @@ const PNGtext = require('png-chunk-text');
11 * @param {string} data Character data to write12 * @param {string} data Character data to write
12 * @returns {Buffer} PNG image buffer with metadata13 * @returns {Buffer} PNG image buffer with metadata
13 */14 */
14const write = (image, data) => {15export const write = (image, data) => {
15 const chunks = extract(image);16 const chunks = extract(new Uint8Array(image));
16 const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');17 const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
1718
18 // Remove existing tEXt chunks19 // Remove existing tEXt chunks
@@ -36,7 +37,9 @@ const write = (image, data) => {
3637
37 const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64');38 const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64');
38 chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData));39 chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData));
39 } catch (error) { }40 } catch (error) {
41 // Ignore errors when adding v3 chunk
42 }
4043
41 const newBuffer = Buffer.from(encode(chunks));44 const newBuffer = Buffer.from(encode(chunks));
42 return newBuffer;45 return newBuffer;
@@ -48,8 +51,8 @@ const write = (image, data) => {
48 * @param {Buffer} image PNG image buffer51 * @param {Buffer} image PNG image buffer
49 * @returns {string} Character data52 * @returns {string} Character data
50 */53 */
51const read = (image) => {54export const read = (image) => {
52 const chunks = extract(image);55 const chunks = extract(new Uint8Array(image));
5356
54 const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));57 const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));
5558
@@ -80,7 +83,7 @@ const read = (image) => {
80 * @param {string} format File format83 * @param {string} format File format
81 * @returns {string} Character data84 * @returns {string} Character data
82 */85 */
83const parse = (cardUrl, format) => {86export const parse = (cardUrl, format) => {
84 let fileFormat = format === undefined ? 'png' : format;87 let fileFormat = format === undefined ? 'png' : format;
8588
86 switch (fileFormat) {89 switch (fileFormat) {
@@ -93,8 +96,3 @@ const parse = (cardUrl, format) => {
93 throw new Error('Unsupported format');96 throw new Error('Unsupported format');
94};97};
9598
96module.exports = {
97 parse,
98 write,
99 read,
100};
src/constants.js+23 -48
@@ -1,18 +1,18 @@
1const PUBLIC_DIRECTORIES = {1export const PUBLIC_DIRECTORIES = {
2 images: 'public/img/',2 images: 'public/img/',
3 backups: 'backups/',3 backups: 'backups/',
4 sounds: 'public/sounds',4 sounds: 'public/sounds',
5 extensions: 'public/scripts/extensions',5 extensions: 'public/scripts/extensions',
6};6};
77
8const SETTINGS_FILE = 'settings.json';8export const SETTINGS_FILE = 'settings.json';
99
10/**10/**
11 * @type {import('./users').UserDirectoryList}11 * @type {import('./users.js').UserDirectoryList}
12 * @readonly12 * @readonly
13 * @enum {string}13 * @enum {string}
14 */14 */
15const USER_DIRECTORY_TEMPLATE = Object.freeze({15export const USER_DIRECTORY_TEMPLATE = Object.freeze({
16 root: '',16 root: '',
17 thumbnails: 'thumbnails',17 thumbnails: 'thumbnails',
18 thumbnailsBg: 'thumbnails/bg',18 thumbnailsBg: 'thumbnails/bg',
@@ -45,10 +45,10 @@ const USER_DIRECTORY_TEMPLATE = Object.freeze({
45});45});
4646
47/**47/**
48 * @type {import('./users').User}48 * @type {import('./users.js').User}
49 * @readonly49 * @readonly
50 */50 */
51const DEFAULT_USER = Object.freeze({51export const DEFAULT_USER = Object.freeze({
52 handle: 'default-user',52 handle: 'default-user',
53 name: 'User',53 name: 'User',
54 created: Date.now(),54 created: Date.now(),
@@ -58,7 +58,7 @@ const DEFAULT_USER = Object.freeze({
58 salt: '',58 salt: '',
59});59});
6060
61const UNSAFE_EXTENSIONS = [61export const UNSAFE_EXTENSIONS = [
62 '.php',62 '.php',
63 '.exe',63 '.exe',
64 '.com',64 '.com',
@@ -135,7 +135,7 @@ const UNSAFE_EXTENSIONS = [
135 '.ws',135 '.ws',
136];136];
137137
138const GEMINI_SAFETY = [138export const GEMINI_SAFETY = [
139 {139 {
140 category: 'HARM_CATEGORY_HARASSMENT',140 category: 'HARM_CATEGORY_HARASSMENT',
141 threshold: 'BLOCK_NONE',141 threshold: 'BLOCK_NONE',
@@ -158,7 +158,7 @@ const GEMINI_SAFETY = [
158 },158 },
159];159];
160160
161const BISON_SAFETY = [161export const BISON_SAFETY = [
162 {162 {
163 category: 'HARM_CATEGORY_DEROGATORY',163 category: 'HARM_CATEGORY_DEROGATORY',
164 threshold: 'BLOCK_NONE',164 threshold: 'BLOCK_NONE',
@@ -185,7 +185,7 @@ const BISON_SAFETY = [
185 },185 },
186];186];
187187
188const CHAT_COMPLETION_SOURCES = {188export const CHAT_COMPLETION_SOURCES = {
189 OPENAI: 'openai',189 OPENAI: 'openai',
190 WINDOWAI: 'windowai',190 WINDOWAI: 'windowai',
191 CLAUDE: 'claude',191 CLAUDE: 'claude',
@@ -205,10 +205,10 @@ const CHAT_COMPLETION_SOURCES = {
205/**205/**
206 * Path to multer file uploads under the data root.206 * Path to multer file uploads under the data root.
207 */207 */
208const UPLOADS_DIRECTORY = '_uploads';208export const UPLOADS_DIRECTORY = '_uploads';
209209
210// TODO: this is copied from the client code; there should be a way to de-duplicate it eventually210// TODO: this is copied from the client code; there should be a way to de-duplicate it eventually
211const TEXTGEN_TYPES = {211export const TEXTGEN_TYPES = {
212 OOBA: 'ooba',212 OOBA: 'ooba',
213 MANCER: 'mancer',213 MANCER: 'mancer',
214 VLLM: 'vllm',214 VLLM: 'vllm',
@@ -225,7 +225,7 @@ const TEXTGEN_TYPES = {
225 HUGGINGFACE: 'huggingface',225 HUGGINGFACE: 'huggingface',
226};226};
227227
228const INFERMATICAI_KEYS = [228export const INFERMATICAI_KEYS = [
229 'model',229 'model',
230 'prompt',230 'prompt',
231 'max_tokens',231 'max_tokens',
@@ -248,7 +248,7 @@ const INFERMATICAI_KEYS = [
248 'logprobs',248 'logprobs',
249];249];
250250
251const FEATHERLESS_KEYS = [251export const FEATHERLESS_KEYS = [
252 'model',252 'model',
253 'prompt',253 'prompt',
254 'best_of',254 'best_of',
@@ -290,9 +290,8 @@ const FEATHERLESS_KEYS = [
290 'guided_whitespace_pattern',290 'guided_whitespace_pattern',
291];291];
292292
293
294// https://dreamgen.com/docs/api#openai-text293// https://dreamgen.com/docs/api#openai-text
295const DREAMGEN_KEYS = [294export const DREAMGEN_KEYS = [
296 'model',295 'model',
297 'prompt',296 'prompt',
298 'max_tokens',297 'max_tokens',
@@ -309,7 +308,7 @@ const DREAMGEN_KEYS = [
309];308];
310309
311// https://docs.together.ai/reference/completions310// https://docs.together.ai/reference/completions
312const TOGETHERAI_KEYS = [311export const TOGETHERAI_KEYS = [
313 'model',312 'model',
314 'prompt',313 'prompt',
315 'max_tokens',314 'max_tokens',
@@ -325,7 +324,7 @@ const TOGETHERAI_KEYS = [
325];324];
326325
327// https://github.com/jmorganca/ollama/blob/main/docs/api.md#request-with-options326// https://github.com/jmorganca/ollama/blob/main/docs/api.md#request-with-options
328const OLLAMA_KEYS = [327export const OLLAMA_KEYS = [
329 'num_predict',328 'num_predict',
330 'num_ctx',329 'num_ctx',
331 'stop',330 'stop',
@@ -345,20 +344,20 @@ const OLLAMA_KEYS = [
345 'min_p',344 'min_p',
346];345];
347346
348const AVATAR_WIDTH = 512;347export const AVATAR_WIDTH = 512;
349const AVATAR_HEIGHT = 768;348export const AVATAR_HEIGHT = 768;
350349
351const OPENROUTER_HEADERS = {350export const OPENROUTER_HEADERS = {
352 'HTTP-Referer': 'https://sillytavern.app',351 'HTTP-Referer': 'https://sillytavern.app',
353 'X-Title': 'SillyTavern',352 'X-Title': 'SillyTavern',
354};353};
355354
356const FEATHERLESS_HEADERS = {355export const FEATHERLESS_HEADERS = {
357 'HTTP-Referer': 'https://sillytavern.app',356 'HTTP-Referer': 'https://sillytavern.app',
358 'X-Title': 'SillyTavern',357 'X-Title': 'SillyTavern',
359};358};
360359
361const OPENROUTER_KEYS = [360export const OPENROUTER_KEYS = [
362 'max_tokens',361 'max_tokens',
363 'temperature',362 'temperature',
364 'top_k',363 'top_k',
@@ -378,7 +377,7 @@ const OPENROUTER_KEYS = [
378];377];
379378
380// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220379// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
381const VLLM_KEYS = [380export const VLLM_KEYS = [
382 'model',381 'model',
383 'prompt',382 'prompt',
384 'best_of',383 'best_of',
@@ -419,27 +418,3 @@ const VLLM_KEYS = [
419 'guided_decoding_backend',418 'guided_decoding_backend',
420 'guided_whitespace_pattern',419 'guided_whitespace_pattern',
421];420];
422
423module.exports = {
424 DEFAULT_USER,
425 SETTINGS_FILE,
426 PUBLIC_DIRECTORIES,
427 USER_DIRECTORY_TEMPLATE,
428 UNSAFE_EXTENSIONS,
429 UPLOADS_DIRECTORY,
430 GEMINI_SAFETY,
431 BISON_SAFETY,
432 TEXTGEN_TYPES,
433 CHAT_COMPLETION_SOURCES,
434 AVATAR_WIDTH,
435 AVATAR_HEIGHT,
436 TOGETHERAI_KEYS,
437 OLLAMA_KEYS,
438 INFERMATICAI_KEYS,
439 DREAMGEN_KEYS,
440 OPENROUTER_HEADERS,
441 OPENROUTER_KEYS,
442 VLLM_KEYS,
443 FEATHERLESS_KEYS,
444 FEATHERLESS_HEADERS,
445};
src/endpoints/anthropic.js+7 -8
@@ -1,9 +1,10 @@
1const { readSecret, SECRET_KEYS } = require('./secrets');1import fetch from 'node-fetch';
2const fetch = require('node-fetch').default;2import express from 'express';
3const express = require('express');
4const { jsonParser } = require('../express-common');
53
6const router = express.Router();4import { readSecret, SECRET_KEYS } from './secrets.js';
5import { jsonParser } from '../express-common.js';
6
7export const router = express.Router();
78
8router.post('/caption-image', jsonParser, async (request, response) => {9router.post('/caption-image', jsonParser, async (request, response) => {
9 try {10 try {
@@ -41,7 +42,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
41 'anthropic-version': '2023-06-01',42 'anthropic-version': '2023-06-01',
42 'x-api-key': request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE),43 'x-api-key': request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE),
43 },44 },
44 timeout: 0,
45 });45 });
4646
47 if (!result.ok) {47 if (!result.ok) {
@@ -50,6 +50,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
50 return response.status(result.status).send({ error: true });50 return response.status(result.status).send({ error: true });
51 }51 }
5252
53 /** @type {any} */
53 const generateResponseJson = await result.json();54 const generateResponseJson = await result.json();
54 const caption = generateResponseJson.content[0].text;55 const caption = generateResponseJson.content[0].text;
55 console.log('Claude response:', generateResponseJson);56 console.log('Claude response:', generateResponseJson);
@@ -64,5 +65,3 @@ router.post('/caption-image', jsonParser, async (request, response) => {
64 response.status(500).send('Internal server error');65 response.status(500).send('Internal server error');
65 }66 }
66});67});
67
68module.exports = { router };
src/endpoints/assets.js+15 -15
@@ -1,13 +1,15 @@
1const path = require('path');1import path from 'node:path';
2const fs = require('fs');2import fs from 'node:fs';
3const mime = require('mime-types');3import { finished } from 'node:stream/promises';
4const express = require('express');4
5const sanitize = require('sanitize-filename');5import mime from 'mime-types';
6const fetch = require('node-fetch').default;6import express from 'express';
7const { finished } = require('stream/promises');7import sanitize from 'sanitize-filename';
8const { UNSAFE_EXTENSIONS } = require('../constants');8import fetch from 'node-fetch';
9const { jsonParser } = require('../express-common');9
10const { clientRelativePath } = require('../util');10import { UNSAFE_EXTENSIONS } from '../constants.js';
11import { jsonParser } from '../express-common.js';
12import { clientRelativePath } from '../util.js';
1113
12const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm', 'character', 'temp'];14const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm', 'character', 'temp'];
1315
@@ -16,7 +18,7 @@ const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm', 'character'
16 * @param {string} inputFilename Input filename18 * @param {string} inputFilename Input filename
17 * @returns {{error: boolean, message?: string}} Whether validation failed, and why if so19 * @returns {{error: boolean, message?: string}} Whether validation failed, and why if so
18 */20 */
19function validateAssetFileName(inputFilename) {21export function validateAssetFileName(inputFilename) {
20 if (!/^[a-zA-Z0-9_\-.]+$/.test(inputFilename)) {22 if (!/^[a-zA-Z0-9_\-.]+$/.test(inputFilename)) {
21 return {23 return {
22 error: true,24 error: true,
@@ -77,7 +79,7 @@ function getFiles(dir, files = []) {
7779
78/**80/**
79 * Ensure that the asset folders exist.81 * Ensure that the asset folders exist.
80 * @param {import('../users').UserDirectoryList} directories - The user's directories82 * @param {import('../users.js').UserDirectoryList} directories - The user's directories
81 */83 */
82function ensureFoldersExist(directories) {84function ensureFoldersExist(directories) {
83 const folderPath = path.join(directories.assets);85 const folderPath = path.join(directories.assets);
@@ -93,7 +95,7 @@ function ensureFoldersExist(directories) {
93 }95 }
94}96}
9597
96const router = express.Router();98export const router = express.Router();
9799
98/**100/**
99 * HTTP POST handler function to retrieve name of all files of a given folder path.101 * HTTP POST handler function to retrieve name of all files of a given folder path.
@@ -366,5 +368,3 @@ router.post('/character', jsonParser, async (request, response) => {
366 return response.sendStatus(500);368 return response.sendStatus(500);
367 }369 }
368});370});
369
370module.exports = { router, validateAssetFileName };
src/endpoints/avatars.js+11 -13
@@ -1,16 +1,16 @@
1const express = require('express');1import path from 'node:path';
2const path = require('path');2import fs from 'node:fs';
3const fs = require('fs');
4const sanitize = require('sanitize-filename');
5const writeFileAtomicSync = require('write-file-atomic').sync;
6const { jsonParser, urlencodedParser } = require('../express-common');
7const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
8const { getImages, tryParse } = require('../util');
93
10// image processing related library imports4import express from 'express';
11const jimp = require('jimp');5import sanitize from 'sanitize-filename';
6import jimp from 'jimp';
7import { sync as writeFileAtomicSync } from 'write-file-atomic';
128
13const router = express.Router();9import { jsonParser, urlencodedParser } from '../express-common.js';
10import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
11import { getImages, tryParse } from '../util.js';
12
13export const router = express.Router();
1414
15router.post('/get', jsonParser, function (request, response) {15router.post('/get', jsonParser, function (request, response) {
16 var images = getImages(request.user.directories.avatars);16 var images = getImages(request.user.directories.avatars);
@@ -58,5 +58,3 @@ router.post('/upload', urlencodedParser, async (request, response) => {
58 return response.status(400).send('Is not a valid image');58 return response.status(400).send('Is not a valid image');
59 }59 }
60});60});
61
62module.exports = { router };
src/endpoints/azure.js+6 -9
@@ -1,9 +1,10 @@
1const { readSecret, SECRET_KEYS } = require('./secrets');1import fetch from 'node-fetch';
2const fetch = require('node-fetch').default;2import { Router } from 'express';
3const express = require('express');
4const { jsonParser } = require('../express-common');
53
6const router = express.Router();4import { readSecret, SECRET_KEYS } from './secrets.js';
5import { jsonParser } from '../express-common.js';
6
7export const router = Router();
78
8router.post('/list', jsonParser, async (req, res) => {9router.post('/list', jsonParser, async (req, res) => {
9 try {10 try {
@@ -86,7 +87,3 @@ router.post('/generate', jsonParser, async (req, res) => {
86 return res.sendStatus(500);87 return res.sendStatus(500);
87 }88 }
88});89});
89
90module.exports = {
91 router,
92};
src/endpoints/backends/chat-completions.js+44 -55
@@ -1,13 +1,41 @@
1const express = require('express');1import process from 'node:process';
2const fetch = require('node-fetch').default;2import express from 'express';
33import fetch from 'node-fetch';
4const { jsonParser } = require('../../express-common');4
5const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');5import { jsonParser } from '../../express-common.js';
6const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');6import {
7const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertAI21Messages, mergeMessages } = require('../../prompt-converters');7 CHAT_COMPLETION_SOURCES,
88 GEMINI_SAFETY,
9const { readSecret, SECRET_KEYS } = require('../secrets');9 BISON_SAFETY,
10const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');10 OPENROUTER_HEADERS,
11} from '../../constants.js';
12import {
13 forwardFetchResponse,
14 getConfigValue,
15 tryParse,
16 uuidv4,
17 mergeObjectWithYaml,
18 excludeKeysByYaml,
19 color,
20} from '../../util.js';
21import {
22 convertClaudeMessages,
23 convertGooglePrompt,
24 convertTextCompletionPrompt,
25 convertCohereMessages,
26 convertMistralMessages,
27 convertAI21Messages,
28 mergeMessages,
29} from '../../prompt-converters.js';
30
31import { readSecret, SECRET_KEYS } from '../secrets.js';
32import {
33 getTokenizerModel,
34 getSentencepiceTokenizer,
35 getTiktokenTokenizer,
36 sentencepieceTokenizers,
37 TEXT_COMPLETION_MODELS,
38} from '../tokenizers.js';
1139
12const API_OPENAI = 'https://api.openai.com/v1';40const API_OPENAI = 'https://api.openai.com/v1';
13const API_CLAUDE = 'https://api.anthropic.com/v1';41const API_CLAUDE = 'https://api.anthropic.com/v1';
@@ -42,43 +70,6 @@ function postProcessPrompt(messages, type, charName, userName) {
42}70}
4371
44/**72/**
45 * Ollama strikes back. Special boy #2's steaming routine.
46 * Wrap this abomination into proper SSE stream, again.
47 * @param {Response} jsonStream JSON stream
48 * @param {import('express').Request} request Express request
49 * @param {import('express').Response} response Express response
50 * @returns {Promise<any>} Nothing valuable
51 */
52async function parseCohereStream(jsonStream, request, response) {
53 try {
54 const stream = new CohereStream({ stream: jsonStream.body, eventShape: { type: 'json', messageTerminator: '\n' } });
55
56 for await (const json of stream.iterMessages()) {
57 if (json.message) {
58 const message = json.message || 'Unknown error';
59 const chunk = { error: { message: message } };
60 response.write(`data: ${JSON.stringify(chunk)}\n\n`);
61 } else if (json.event_type === 'text-generation') {
62 const text = json.text || '';
63 const chunk = { choices: [{ text }] };
64 response.write(`data: ${JSON.stringify(chunk)}\n\n`);
65 }
66 }
67
68 console.log('Streaming request finished');
69 response.write('data: [DONE]\n\n');
70 response.end();
71 } catch (error) {
72 console.log('Error forwarding streaming response:', error);
73 if (!response.headersSent) {
74 return response.status(500).send({ error: true });
75 } else {
76 return response.end();
77 }
78 }
79}
80
81/**
82 * Sends a request to Claude API.73 * Sends a request to Claude API.
83 * @param {express.Request} request Express request74 * @param {express.Request} request Express request
84 * @param {express.Response} response Express response75 * @param {express.Response} response Express response
@@ -161,7 +152,6 @@ async function sendClaudeRequest(request, response) {
161 'x-api-key': apiKey,152 'x-api-key': apiKey,
162 ...additionalHeaders,153 ...additionalHeaders,
163 },154 },
164 timeout: 0,
165 });155 });
166156
167 if (request.body.stream) {157 if (request.body.stream) {
@@ -174,6 +164,7 @@ async function sendClaudeRequest(request, response) {
174 return response.status(generateResponse.status).send({ error: true });164 return response.status(generateResponse.status).send({ error: true });
175 }165 }
176166
167 /** @type {any} */
177 const generateResponseJson = await generateResponse.json();168 const generateResponseJson = await generateResponse.json();
178 const responseText = generateResponseJson?.content?.[0]?.text || '';169 const responseText = generateResponseJson?.content?.[0]?.text || '';
179 console.log('Claude response:', generateResponseJson);170 console.log('Claude response:', generateResponseJson);
@@ -221,7 +212,6 @@ async function sendScaleRequest(request, response) {
221 'Content-Type': 'application/json',212 'Content-Type': 'application/json',
222 'Authorization': `Basic ${apiKey}`,213 'Authorization': `Basic ${apiKey}`,
223 },214 },
224 timeout: 0,
225 });215 });
226216
227 if (!generateResponse.ok) {217 if (!generateResponse.ok) {
@@ -229,6 +219,7 @@ async function sendScaleRequest(request, response) {
229 return response.status(500).send({ error: true });219 return response.status(500).send({ error: true });
230 }220 }
231221
222 /** @type {any} */
232 const generateResponseJson = await generateResponse.json();223 const generateResponseJson = await generateResponse.json();
233 console.log('Scale response:', generateResponseJson);224 console.log('Scale response:', generateResponseJson);
234225
@@ -344,7 +335,6 @@ async function sendMakerSuiteRequest(request, response) {
344 'Content-Type': 'application/json',335 'Content-Type': 'application/json',
345 },336 },
346 signal: controller.signal,337 signal: controller.signal,
347 timeout: 0,
348 });338 });
349 // have to do this because of their busted ass streaming endpoint339 // have to do this because of their busted ass streaming endpoint
350 if (stream) {340 if (stream) {
@@ -363,6 +353,7 @@ async function sendMakerSuiteRequest(request, response) {
363 return response.status(generateResponse.status).send({ error: true });353 return response.status(generateResponse.status).send({ error: true });
364 }354 }
365355
356 /** @type {any} */
366 const generateResponseJson = await generateResponse.json();357 const generateResponseJson = await generateResponse.json();
367358
368 const candidates = generateResponseJson?.candidates;359 const candidates = generateResponseJson?.candidates;
@@ -626,7 +617,7 @@ async function sendCohereRequest(request, response) {
626 }617 }
627}618}
628619
629const router = express.Router();620export const router = express.Router();
630621
631router.post('/status', jsonParser, async function (request, response_getstatus_openai) {622router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
632 if (!request.body) return response_getstatus_openai.sendStatus(400);623 if (!request.body) return response_getstatus_openai.sendStatus(400);
@@ -685,6 +676,7 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
685 });676 });
686677
687 if (response.ok) {678 if (response.ok) {
679 /** @type {any} */
688 const data = await response.json();680 const data = await response.json();
689 response_getstatus_openai.send(data);681 response_getstatus_openai.send(data);
690682
@@ -988,7 +980,6 @@ router.post('/generate', jsonParser, function (request, response) {
988 },980 },
989 body: JSON.stringify(requestBody),981 body: JSON.stringify(requestBody),
990 signal: controller.signal,982 signal: controller.signal,
991 timeout: 0,
992 };983 };
993984
994 console.log(requestBody);985 console.log(requestBody);
@@ -1014,6 +1005,7 @@ router.post('/generate', jsonParser, function (request, response) {
1014 }1005 }
10151006
1016 if (fetchResponse.ok) {1007 if (fetchResponse.ok) {
1008 /** @type {any} */
1017 let json = await fetchResponse.json();1009 let json = await fetchResponse.json();
1018 response.send(json);1010 response.send(json);
1019 console.log(json);1011 console.log(json);
@@ -1069,6 +1061,3 @@ router.post('/generate', jsonParser, function (request, response) {
1069 }1061 }
1070});1062});
10711063
1072module.exports = {
1073 router,
1074};
src/endpoints/backends/kobold.js+10 -11
@@ -1,13 +1,13 @@
1const express = require('express');1import fs from 'node:fs';
2const fetch = require('node-fetch').default;2import express from 'express';
3const fs = require('fs');3import fetch from 'node-fetch';
44
5const { jsonParser, urlencodedParser } = require('../../express-common');5import { jsonParser, urlencodedParser } from '../../express-common.js';
6const { forwardFetchResponse, delay } = require('../../util');6import { forwardFetchResponse, delay } from '../../util.js';
7const { getOverrideHeaders, setAdditionalHeaders, setAdditionalHeadersByType } = require('../../additional-headers');7import { getOverrideHeaders, setAdditionalHeaders, setAdditionalHeadersByType } from '../../additional-headers.js';
8const { TEXTGEN_TYPES } = require('../../constants');8import { TEXTGEN_TYPES } from '../../constants.js';
99
10const router = express.Router();10export const router = express.Router();
1111
12router.post('/generate', jsonParser, async function (request, response_generate) {12router.post('/generate', jsonParser, async function (request, response_generate) {
13 if (!request.body) return response_generate.sendStatus(400);13 if (!request.body) return response_generate.sendStatus(400);
@@ -96,7 +96,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
96 for (let i = 0; i < MAX_RETRIES; i++) {96 for (let i = 0; i < MAX_RETRIES; i++) {
97 try {97 try {
98 const url = request.body.streaming ? `${request.body.api_server}/extra/generate/stream` : `${request.body.api_server}/v1/generate`;98 const url = request.body.streaming ? `${request.body.api_server}/extra/generate/stream` : `${request.body.api_server}/v1/generate`;
99 const response = await fetch(url, { method: 'POST', timeout: 0, ...args });99 const response = await fetch(url, { method: 'POST', ...args });
100100
101 if (request.body.streaming) {101 if (request.body.streaming) {
102 // Pipe remote SSE stream to Express response102 // Pipe remote SSE stream to Express response
@@ -156,6 +156,7 @@ router.post('/status', jsonParser, async function (request, response) {
156156
157 const result = {};157 const result = {};
158158
159 /** @type {any} */
159 const [koboldUnitedResponse, koboldExtraResponse, koboldModelResponse] = await Promise.all([160 const [koboldUnitedResponse, koboldExtraResponse, koboldModelResponse] = await Promise.all([
160 // We catch errors both from the response not having a successful HTTP status and from JSON parsing failing161 // We catch errors both from the response not having a successful HTTP status and from JSON parsing failing
161162
@@ -237,5 +238,3 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
237 response.status(500).send('Internal server error');238 response.status(500).send('Internal server error');
238 }239 }
239});240});
240
241module.exports = { router };
src/endpoints/backends/scale-alt.js+6 -9
@@ -1,11 +1,10 @@
1const express = require('express');1import express from 'express';
2const fetch = require('node-fetch').default;2import fetch from 'node-fetch';
33
4const { jsonParser } = require('../../express-common');4import { jsonParser } from '../../express-common.js';
5import { readSecret, SECRET_KEYS } from '../secrets.js';
56
6const { readSecret, SECRET_KEYS } = require('../secrets');7export const router = express.Router();
7
8const router = express.Router();
98
10router.post('/generate', jsonParser, async function (request, response) {9router.post('/generate', jsonParser, async function (request, response) {
11 if (!request.body) return response.sendStatus(400);10 if (!request.body) return response.sendStatus(400);
@@ -71,7 +70,6 @@ router.post('/generate', jsonParser, async function (request, response) {
71 'Content-Type': 'application/json',70 'Content-Type': 'application/json',
72 'cookie': `_jwt=${cookie}`,71 'cookie': `_jwt=${cookie}`,
73 },72 },
74 timeout: 0,
75 body: JSON.stringify(body),73 body: JSON.stringify(body),
76 });74 });
7775
@@ -81,6 +79,7 @@ router.post('/generate', jsonParser, async function (request, response) {
81 return response.status(500).send({ error: { message: result.statusText } });79 return response.status(500).send({ error: { message: result.statusText } });
82 }80 }
8381
82 /** @type {any} */
84 const data = await result.json();83 const data = await result.json();
85 const output = data?.result?.data?.json?.outputs?.[0] || '';84 const output = data?.result?.data?.json?.outputs?.[0] || '';
8685
@@ -97,5 +96,3 @@ router.post('/generate', jsonParser, async function (request, response) {
97 return response.sendStatus(500);96 return response.sendStatus(500);
98 }97 }
99});98});
100
101module.exports = { router };
src/endpoints/backends/text-completions.js+31 -19
@@ -1,14 +1,23 @@
1const express = require('express');1import { Readable } from 'node:stream';
2const fetch = require('node-fetch').default;2import fetch from 'node-fetch';
3const _ = require('lodash');3import express from 'express';
4const Readable = require('stream').Readable;4import _ from 'lodash';
55
6const { jsonParser } = require('../../express-common');6import { jsonParser } from '../../express-common.js';
7const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants');7import {
8const { forwardFetchResponse, trimV1, getConfigValue } = require('../../util');8 TEXTGEN_TYPES,
9const { setAdditionalHeaders } = require('../../additional-headers');9 TOGETHERAI_KEYS,
1010 OLLAMA_KEYS,
11const router = express.Router();11 INFERMATICAI_KEYS,
12 OPENROUTER_KEYS,
13 VLLM_KEYS,
14 DREAMGEN_KEYS,
15 FEATHERLESS_KEYS,
16} from '../../constants.js';
17import { forwardFetchResponse, trimV1, getConfigValue } from '../../util.js';
18import { setAdditionalHeaders } from '../../additional-headers.js';
19
20export const router = express.Router();
1221
13/**22/**
14 * Special boy's steaming routine. Wrap this abomination into proper SSE stream.23 * Special boy's steaming routine. Wrap this abomination into proper SSE stream.
@@ -19,6 +28,10 @@ const router = express.Router();
19 */28 */
20async function parseOllamaStream(jsonStream, request, response) {29async function parseOllamaStream(jsonStream, request, response) {
21 try {30 try {
31 if (!jsonStream.body) {
32 throw new Error('No body in the response');
33 }
34
22 let partialData = '';35 let partialData = '';
23 jsonStream.body.on('data', (data) => {36 jsonStream.body.on('data', (data) => {
24 const chunk = data.toString();37 const chunk = data.toString();
@@ -144,6 +157,7 @@ router.post('/status', jsonParser, async function (request, response) {
144 return response.status(400);157 return response.status(400);
145 }158 }
146159
160 /** @type {any} */
147 let data = await modelsReply.json();161 let data = await modelsReply.json();
148162
149 if (request.body.legacy_api) {163 if (request.body.legacy_api) {
@@ -181,6 +195,7 @@ router.post('/status', jsonParser, async function (request, response) {
181 const modelInfoReply = await fetch(modelInfoUrl, args);195 const modelInfoReply = await fetch(modelInfoUrl, args);
182196
183 if (modelInfoReply.ok) {197 if (modelInfoReply.ok) {
198 /** @type {any} */
184 const modelInfo = await modelInfoReply.json();199 const modelInfo = await modelInfoReply.json();
185 console.log('Ooba model info:', modelInfo);200 console.log('Ooba model info:', modelInfo);
186201
@@ -197,6 +212,7 @@ router.post('/status', jsonParser, async function (request, response) {
197 const modelInfoReply = await fetch(modelInfoUrl, args);212 const modelInfoReply = await fetch(modelInfoUrl, args);
198213
199 if (modelInfoReply.ok) {214 if (modelInfoReply.ok) {
215 /** @type {any} */
200 const modelInfo = await modelInfoReply.json();216 const modelInfo = await modelInfoReply.json();
201 console.log('Tabby model info:', modelInfo);217 console.log('Tabby model info:', modelInfo);
202218
@@ -350,6 +366,7 @@ router.post('/generate', jsonParser, async function (request, response) {
350 const completionsReply = await fetch(url, args);366 const completionsReply = await fetch(url, args);
351367
352 if (completionsReply.ok) {368 if (completionsReply.ok) {
369 /** @type {any} */
353 const data = await completionsReply.json();370 const data = await completionsReply.json();
354 console.log('Endpoint response:', data);371 console.log('Endpoint response:', data);
355372
@@ -406,7 +423,6 @@ ollama.post('/download', jsonParser, async function (request, response) {
406 name: name,423 name: name,
407 stream: false,424 stream: false,
408 }),425 }),
409 timeout: 0,
410 });426 });
411427
412 if (!fetchResponse.ok) {428 if (!fetchResponse.ok) {
@@ -439,7 +455,6 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
439 images: [request.body.image],455 images: [request.body.image],
440 stream: false,456 stream: false,
441 }),457 }),
442 timeout: 0,
443 });458 });
444459
445 if (!fetchResponse.ok) {460 if (!fetchResponse.ok) {
@@ -447,6 +462,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
447 return response.status(500).send({ error: true });462 return response.status(500).send({ error: true });
448 }463 }
449464
465 /** @type {any} */
450 const data = await fetchResponse.json();466 const data = await fetchResponse.json();
451 console.log('Ollama caption response:', data);467 console.log('Ollama caption response:', data);
452468
@@ -478,7 +494,6 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
478 const fetchResponse = await fetch(`${baseUrl}/completion`, {494 const fetchResponse = await fetch(`${baseUrl}/completion`, {
479 method: 'POST',495 method: 'POST',
480 headers: { 'Content-Type': 'application/json' },496 headers: { 'Content-Type': 'application/json' },
481 timeout: 0,
482 body: JSON.stringify({497 body: JSON.stringify({
483 prompt: `USER:[img-1]${String(request.body.prompt).trim()}\nASSISTANT:`,498 prompt: `USER:[img-1]${String(request.body.prompt).trim()}\nASSISTANT:`,
484 image_data: [{ data: request.body.image, id: 1 }],499 image_data: [{ data: request.body.image, id: 1 }],
@@ -493,6 +508,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
493 return response.status(500).send({ error: true });508 return response.status(500).send({ error: true });
494 }509 }
495510
511 /** @type {any} */
496 const data = await fetchResponse.json();512 const data = await fetchResponse.json();
497 console.log('LlamaCpp caption response:', data);513 console.log('LlamaCpp caption response:', data);
498514
@@ -522,7 +538,6 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
522538
523 const fetchResponse = await fetch(`${baseUrl}/props`, {539 const fetchResponse = await fetch(`${baseUrl}/props`, {
524 method: 'GET',540 method: 'GET',
525 timeout: 0,
526 });541 });
527542
528 if (!fetchResponse.ok) {543 if (!fetchResponse.ok) {
@@ -557,7 +572,6 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
557 if (request.body.action === 'info') {572 if (request.body.action === 'info') {
558 fetchResponse = await fetch(`${baseUrl}/slots`, {573 fetchResponse = await fetch(`${baseUrl}/slots`, {
559 method: 'GET',574 method: 'GET',
560 timeout: 0,
561 });575 });
562 } else {576 } else {
563 if (!/^\d+$/.test(request.body.id_slot)) {577 if (!/^\d+$/.test(request.body.id_slot)) {
@@ -570,7 +584,6 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
570 fetchResponse = await fetch(`${baseUrl}/slots/${request.body.id_slot}?action=${request.body.action}`, {584 fetchResponse = await fetch(`${baseUrl}/slots/${request.body.id_slot}?action=${request.body.action}`, {
571 method: 'POST',585 method: 'POST',
572 headers: { 'Content-Type': 'application/json' },586 headers: { 'Content-Type': 'application/json' },
573 timeout: 0,
574 body: JSON.stringify({587 body: JSON.stringify({
575 filename: request.body.action !== 'erase' ? `${request.body.filename}` : undefined,588 filename: request.body.action !== 'erase' ? `${request.body.filename}` : undefined,
576 }),589 }),
@@ -614,6 +627,7 @@ tabby.post('/download', jsonParser, async function (request, response) {
614 });627 });
615628
616 if (permissionResponse.ok) {629 if (permissionResponse.ok) {
630 /** @type {any} */
617 const permissionJson = await permissionResponse.json();631 const permissionJson = await permissionResponse.json();
618632
619 if (permissionJson['permission'] !== 'admin') {633 if (permissionJson['permission'] !== 'admin') {
@@ -641,5 +655,3 @@ tabby.post('/download', jsonParser, async function (request, response) {
641router.use('/ollama', ollama);655router.use('/ollama', ollama);
642router.use('/llamacpp', llamacpp);656router.use('/llamacpp', llamacpp);
643router.use('/tabby', tabby);657router.use('/tabby', tabby);
644
645module.exports = { router };
src/endpoints/backgrounds.js+9 -10
@@ -1,13 +1,14 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');
4const sanitize = require('sanitize-filename');
53
6const { jsonParser, urlencodedParser } = require('../express-common');4import express from 'express';
7const { invalidateThumbnail } = require('./thumbnails');5import sanitize from 'sanitize-filename';
8const { getImages } = require('../util');
96
10const router = express.Router();7import { jsonParser, urlencodedParser } from '../express-common.js';
8import { invalidateThumbnail } from './thumbnails.js';
9import { getImages } from '../util.js';
10
11export const router = express.Router();
1112
12router.post('/all', jsonParser, function (request, response) {13router.post('/all', jsonParser, function (request, response) {
13 var images = getImages(request.user.directories.backgrounds);14 var images = getImages(request.user.directories.backgrounds);
@@ -72,5 +73,3 @@ router.post('/upload', urlencodedParser, function (request, response) {
72 response.sendStatus(500);73 response.sendStatus(500);
73 }74 }
74});75});
75
76module.exports = { router };
src/endpoints/caption.js+6 -8
@@ -1,23 +1,23 @@
1const express = require('express');1import express from 'express';
2const { jsonParser } = require('../express-common');2import { jsonParser } from '../express-common.js';
3import { getPipeline, getRawImage } from '../transformers.mjs';
34
4const TASK = 'image-to-text';5const TASK = 'image-to-text';
56
6const router = express.Router();7export const router = express.Router();
78
8router.post('/', jsonParser, async (req, res) => {9router.post('/', jsonParser, async (req, res) => {
9 try {10 try {
10 const { image } = req.body;11 const { image } = req.body;
1112
12 const module = await import('../transformers.mjs');13 const rawImage = await getRawImage(image);
13 const rawImage = await module.default.getRawImage(image);
1414
15 if (!rawImage) {15 if (!rawImage) {
16 console.log('Failed to parse captioned image');16 console.log('Failed to parse captioned image');
17 return res.sendStatus(400);17 return res.sendStatus(400);
18 }18 }
1919
20 const pipe = await module.default.getPipeline(TASK);20 const pipe = await getPipeline(TASK);
21 const result = await pipe(rawImage);21 const result = await pipe(rawImage);
22 const text = result[0].generated_text;22 const text = result[0].generated_text;
23 console.log('Image caption:', text);23 console.log('Image caption:', text);
@@ -28,5 +28,3 @@ router.post('/', jsonParser, async (req, res) => {
28 return res.sendStatus(500);28 return res.sendStatus(500);
29 }29 }
30});30});
31
32module.exports = { router };
src/endpoints/characters.js+30 -31
@@ -1,24 +1,25 @@
1const path = require('path');1import path from 'node:path';
2const fs = require('fs');2import fs from 'node:fs';
3const fsPromises = require('fs').promises;3import { promises as fsPromises } from 'node:fs';
4const readline = require('readline');4import readline from 'node:readline';
5const express = require('express');5import { Buffer } from 'node:buffer';
6const sanitize = require('sanitize-filename');6
7const writeFileAtomicSync = require('write-file-atomic').sync;7import express from 'express';
8const yaml = require('yaml');8import sanitize from 'sanitize-filename';
9const _ = require('lodash');9import { sync as writeFileAtomicSync } from 'write-file-atomic';
10const mime = require('mime-types');10import yaml from 'yaml';
1111import _ from 'lodash';
12const jimp = require('jimp');12import mime from 'mime-types';
1313import jimp from 'jimp';
14const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');14
15const { jsonParser, urlencodedParser } = require('../express-common');15import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
16const { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer } = require('../util');16import { jsonParser, urlencodedParser } from '../express-common.js';
17const { TavernCardValidator } = require('../validator/TavernCardValidator');17import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer } from '../util.js';
18const characterCardParser = require('../character-card-parser.js');18import { TavernCardValidator } from '../validator/TavernCardValidator.js';
19const { readWorldInfoFile } = require('./worldinfo');19import { parse, write } from '../character-card-parser.js';
20const { invalidateThumbnail } = require('./thumbnails');20import { readWorldInfoFile } from './worldinfo.js';
21const { importRisuSprites } = require('./sprites');21import { invalidateThumbnail } from './thumbnails.js';
22import { importRisuSprites } from './sprites.js';
22const defaultAvatarPath = './public/img/ai4.png';23const defaultAvatarPath = './public/img/ai4.png';
2324
24// KV-store for parsed character data25// KV-store for parsed character data
@@ -37,7 +38,7 @@ async function readCharacterData(inputFile, inputFormat = 'png') {
37 return characterDataCache.get(cacheKey);38 return characterDataCache.get(cacheKey);
38 }39 }
3940
40 const result = characterCardParser.parse(inputFile, inputFormat);41 const result = parse(inputFile, inputFormat);
41 characterDataCache.set(cacheKey, result);42 characterDataCache.set(cacheKey, result);
42 return result;43 return result;
43}44}
@@ -76,7 +77,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
76 const inputImage = await getInputImage();77 const inputImage = await getInputImage();
7778
78 // Get the chunks79 // Get the chunks
79 const outputImage = characterCardParser.write(inputImage, data);80 const outputImage = write(inputImage, data);
80 const outputImagePath = path.join(request.user.directories.characters, `${outputFile}.png`);81 const outputImagePath = path.join(request.user.directories.characters, `${outputFile}.png`);
8182
82 writeFileAtomicSync(outputImagePath, outputImage);83 writeFileAtomicSync(outputImagePath, outputImage);
@@ -188,7 +189,7 @@ const calculateDataSize = (data) => {
188 * processCharacter - Process a given character, read its data and calculate its statistics.189 * processCharacter - Process a given character, read its data and calculate its statistics.
189 *190 *
190 * @param {string} item The name of the character.191 * @param {string} item The name of the character.
191 * @param {import('../users').UserDirectoryList} directories User directories192 * @param {import('../users.js').UserDirectoryList} directories User directories
192 * @return {Promise<object>} A Promise that resolves when the character processing is done.193 * @return {Promise<object>} A Promise that resolves when the character processing is done.
193 */194 */
194const processCharacter = async (item, directories) => {195const processCharacter = async (item, directories) => {
@@ -232,7 +233,7 @@ const processCharacter = async (item, directories) => {
232/**233/**
233 * Convert a character object to Spec V2 format.234 * Convert a character object to Spec V2 format.
234 * @param {object} jsonObject Character object235 * @param {object} jsonObject Character object
235 * @param {import('../users').UserDirectoryList} directories User directories236 * @param {import('../users.js').UserDirectoryList} directories User directories
236 * @param {boolean} hoistDate Will set the chat and create_date fields to the current date if they are missing237 * @param {boolean} hoistDate Will set the chat and create_date fields to the current date if they are missing
237 * @returns {object} Character object in Spec V2 format238 * @returns {object} Character object in Spec V2 format
238 */239 */
@@ -252,7 +253,7 @@ function getCharaCardV2(jsonObject, directories, hoistDate = true) {
252/**253/**
253 * Convert a character object to Spec V2 format.254 * Convert a character object to Spec V2 format.
254 * @param {object} char Character object255 * @param {object} char Character object
255 * @param {import('../users').UserDirectoryList} directories User directories256 * @param {import('../users.js').UserDirectoryList} directories User directories
256 * @returns {object} Character object in Spec V2 format257 * @returns {object} Character object in Spec V2 format
257 */258 */
258function convertToV2(char, directories) {259function convertToV2(char, directories) {
@@ -342,7 +343,7 @@ function readFromV2(char) {
342/**343/**
343 * Format character data to Spec V2 format.344 * Format character data to Spec V2 format.
344 * @param {object} data Character data345 * @param {object} data Character data
345 * @param {import('../users').UserDirectoryList} directories User directories346 * @param {import('../users.js').UserDirectoryList} directories User directories
346 * @returns347 * @returns
347 */348 */
348function charaFormatData(data, directories) {349function charaFormatData(data, directories) {
@@ -715,7 +716,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
715 return '';716 return '';
716}717}
717718
718const router = express.Router();719export const router = express.Router();
719720
720router.post('/create', urlencodedParser, async function (request, response) {721router.post('/create', urlencodedParser, async function (request, response) {
721 try {722 try {
@@ -1081,7 +1082,7 @@ router.post('/chats', jsonParser, async function (request, response) {
1081/**1082/**
1082 * Gets the name for the uploaded PNG file.1083 * Gets the name for the uploaded PNG file.
1083 * @param {string} file File name1084 * @param {string} file File name
1084 * @param {import('../users').UserDirectoryList} directories User directories1085 * @param {import('../users.js').UserDirectoryList} directories User directories
1085 * @returns {string} - The name for the uploaded PNG file1086 * @returns {string} - The name for the uploaded PNG file
1086 */1087 */
1087function getPngName(file, directories) {1088function getPngName(file, directories) {
@@ -1231,5 +1232,3 @@ router.post('/export', jsonParser, async function (request, response) {
1231 response.sendStatus(500);1232 response.sendStatus(500);
1232 }1233 }
1233});1234});
1234
1235module.exports = { router };
src/endpoints/chats.js+12 -12
@@ -1,13 +1,15 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const readline = require('readline');3import readline from 'node:readline';
4const express = require('express');4import process from 'node:process';
5const sanitize = require('sanitize-filename');
6const writeFileAtomicSync = require('write-file-atomic').sync;
7const _ = require('lodash');
85
9const { jsonParser, urlencodedParser } = require('../express-common');6import express from 'express';
10const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util');7import sanitize from 'sanitize-filename';
8import { sync as writeFileAtomicSync } from 'write-file-atomic';
9import _ from 'lodash';
10
11import { jsonParser, urlencodedParser } from '../express-common.js';
12import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } from '../util.js';
1113
12/**14/**
13 * Saves a chat to the backups directory.15 * Saves a chat to the backups directory.
@@ -188,7 +190,7 @@ function flattenChubChat(userName, characterName, lines) {
188 return (lines ?? []).map(convert).join('\n');190 return (lines ?? []).map(convert).join('\n');
189}191}
190192
191const router = express.Router();193export const router = express.Router();
192194
193router.post('/save', jsonParser, function (request, response) {195router.post('/save', jsonParser, function (request, response) {
194 try {196 try {
@@ -514,5 +516,3 @@ router.post('/group/save', jsonParser, (request, response) => {
514 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData);516 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData);
515 return response.send({ ok: true });517 return response.send({ ok: true });
516});518});
517
518module.exports = { router };
src/endpoints/classify.js+7 -9
@@ -1,9 +1,11 @@
1const express = require('express');1import express from 'express';
2const { jsonParser } = require('../express-common');2
3import { getPipeline } from '../transformers.mjs';
4import { jsonParser } from '../express-common.js';
35
4const TASK = 'text-classification';6const TASK = 'text-classification';
57
6const router = express.Router();8export const router = express.Router();
79
8/**10/**
9 * @type {Map<string, object>} Cache for classification results11 * @type {Map<string, object>} Cache for classification results
@@ -12,8 +14,7 @@ const cacheObject = new Map();
1214
13router.post('/labels', jsonParser, async (req, res) => {15router.post('/labels', jsonParser, async (req, res) => {
14 try {16 try {
15 const module = await import('../transformers.mjs');17 const pipe = await getPipeline(TASK);
16 const pipe = await module.default.getPipeline(TASK);
17 const result = Object.keys(pipe.model.config.label2id);18 const result = Object.keys(pipe.model.config.label2id);
18 return res.json({ labels: result });19 return res.json({ labels: result });
19 } catch (error) {20 } catch (error) {
@@ -35,8 +36,7 @@ router.post('/', jsonParser, async (req, res) => {
35 if (cacheObject.has(text)) {36 if (cacheObject.has(text)) {
36 return cacheObject.get(text);37 return cacheObject.get(text);
37 } else {38 } else {
38 const module = await import('../transformers.mjs');39 const pipe = await getPipeline(TASK);
39 const pipe = await module.default.getPipeline(TASK);
40 const result = await pipe(text, { topk: 5 });40 const result = await pipe(text, { topk: 5 });
41 result.sort((a, b) => b.score - a.score);41 result.sort((a, b) => b.score - a.score);
42 cacheObject.set(text, result);42 cacheObject.set(text, result);
@@ -54,5 +54,3 @@ router.post('/', jsonParser, async (req, res) => {
54 return res.sendStatus(500);54 return res.sendStatus(500);
55 }55 }
56});56});
57
58module.exports = { router };
src/endpoints/content-manager.js+27 -29
@@ -1,16 +1,21 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3import process from 'node:process';
4const fetch = require('node-fetch').default;4import { Buffer } from 'node:buffer';
5const sanitize = require('sanitize-filename');5
6const { getConfigValue, color } = require('../util');6import express from 'express';
7const { jsonParser } = require('../express-common');7import fetch from 'node-fetch';
8const writeFileAtomicSync = require('write-file-atomic').sync;8import sanitize from 'sanitize-filename';
9import { sync as writeFileAtomicSync } from 'write-file-atomic';
10
11import { getConfigValue, color } from '../util.js';
12import { jsonParser } from '../express-common.js';
13import { write } from '../character-card-parser.js';
14
9const contentDirectory = path.join(process.cwd(), 'default/content');15const contentDirectory = path.join(process.cwd(), 'default/content');
10const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');16const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');
11const contentIndexPath = path.join(contentDirectory, 'index.json');17const contentIndexPath = path.join(contentDirectory, 'index.json');
12const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');18const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
13const characterCardParser = require('../character-card-parser.js');
1419
15const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);20const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
1621
@@ -26,7 +31,7 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo
26 * @typedef {string} ContentType31 * @typedef {string} ContentType
27 * @enum {string}32 * @enum {string}
28 */33 */
29const CONTENT_TYPES = {34export const CONTENT_TYPES = {
30 SETTINGS: 'settings',35 SETTINGS: 'settings',
31 CHARACTER: 'character',36 CHARACTER: 'character',
32 SPRITES: 'sprites',37 SPRITES: 'sprites',
@@ -48,10 +53,10 @@ const CONTENT_TYPES = {
4853
49/**54/**
50 * Gets the default presets from the content directory.55 * Gets the default presets from the content directory.
51 * @param {import('../users').UserDirectoryList} directories User directories56 * @param {import('../users.js').UserDirectoryList} directories User directories
52 * @returns {object[]} Array of default presets57 * @returns {object[]} Array of default presets
53 */58 */
54function getDefaultPresets(directories) {59export function getDefaultPresets(directories) {
55 try {60 try {
56 const contentIndex = getContentIndex();61 const contentIndex = getContentIndex();
57 const presets = [];62 const presets = [];
@@ -76,7 +81,7 @@ function getDefaultPresets(directories) {
76 * @param {string} filename Name of the file to get81 * @param {string} filename Name of the file to get
77 * @returns {object | null} JSON object or null if the file doesn't exist82 * @returns {object | null} JSON object or null if the file doesn't exist
78 */83 */
79function getDefaultPresetFile(filename) {84export function getDefaultPresetFile(filename) {
80 try {85 try {
81 const contentPath = path.join(contentDirectory, filename);86 const contentPath = path.join(contentDirectory, filename);
8287
@@ -95,7 +100,7 @@ function getDefaultPresetFile(filename) {
95/**100/**
96 * Seeds content for a user.101 * Seeds content for a user.
97 * @param {ContentItem[]} contentIndex Content index102 * @param {ContentItem[]} contentIndex Content index
98 * @param {import('../users').UserDirectoryList} directories User directories103 * @param {import('../users.js').UserDirectoryList} directories User directories
99 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)104 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)
100 * @returns {Promise<boolean>} Whether any content was added105 * @returns {Promise<boolean>} Whether any content was added
101 */106 */
@@ -154,11 +159,11 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
154159
155/**160/**
156 * Checks for new content and seeds it for all users.161 * Checks for new content and seeds it for all users.
157 * @param {import('../users').UserDirectoryList[]} directoriesList List of user directories162 * @param {import('../users.js').UserDirectoryList[]} directoriesList List of user directories
158 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)163 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)
159 * @returns {Promise<void>}164 * @returns {Promise<void>}
160 */165 */
161async function checkForNewContent(directoriesList, forceCategories = []) {166export async function checkForNewContent(directoriesList, forceCategories = []) {
162 try {167 try {
163 const contentCheckSkip = getConfigValue('skipContentCheck', false);168 const contentCheckSkip = getConfigValue('skipContentCheck', false);
164 if (contentCheckSkip && forceCategories?.length === 0) {169 if (contentCheckSkip && forceCategories?.length === 0) {
@@ -224,7 +229,7 @@ function getContentIndex() {
224 * @param {'json'|'string'|'raw'} format Format of content229 * @param {'json'|'string'|'raw'} format Format of content
225 * @returns {string[]|Buffer[]} Array of content230 * @returns {string[]|Buffer[]} Array of content
226 */231 */
227function getContentOfType(type, format) {232export function getContentOfType(type, format) {
228 const contentIndex = getContentIndex();233 const contentIndex = getContentIndex();
229 const indexItems = contentIndex.filter((item) => item.type === type && item.folder);234 const indexItems = contentIndex.filter((item) => item.type === type && item.folder);
230 const files = [];235 const files = [];
@@ -256,7 +261,7 @@ function getContentOfType(type, format) {
256/**261/**
257 * Gets the target directory for the specified asset type.262 * Gets the target directory for the specified asset type.
258 * @param {ContentType} type Asset type263 * @param {ContentType} type Asset type
259 * @param {import('../users').UserDirectoryList} directories User directories264 * @param {import('../users.js').UserDirectoryList} directories User directories
260 * @returns {string | null} Target directory265 * @returns {string | null} Target directory
261 */266 */
262function getTargetByType(type, directories) {267function getTargetByType(type, directories) {
@@ -375,6 +380,7 @@ async function downloadPygmalionCharacter(id) {
375 throw new Error('Failed to download character');380 throw new Error('Failed to download character');
376 }381 }
377382
383 /** @type {any} */
378 const jsonData = await result.json();384 const jsonData = await result.json();
379 const characterData = jsonData?.character;385 const characterData = jsonData?.character;
380386
@@ -394,7 +400,7 @@ async function downloadPygmalionCharacter(id) {
394 const avatarResult = await fetch(avatarUrl);400 const avatarResult = await fetch(avatarUrl);
395 const avatarBuffer = await avatarResult.buffer();401 const avatarBuffer = await avatarResult.buffer();
396402
397 const cardBuffer = characterCardParser.write(avatarBuffer, JSON.stringify(characterData));403 const cardBuffer = write(avatarBuffer, JSON.stringify(characterData));
398404
399 return {405 return {
400 buffer: cardBuffer,406 buffer: cardBuffer,
@@ -467,6 +473,7 @@ async function downloadJannyCharacter(uuid) {
467 });473 });
468474
469 if (result.ok) {475 if (result.ok) {
476 /** @type {any} */
470 const downloadResult = await result.json();477 const downloadResult = await result.json();
471 if (downloadResult.status === 'ok') {478 if (downloadResult.status === 'ok') {
472 const imageResult = await fetch(downloadResult.downloadUrl);479 const imageResult = await fetch(downloadResult.downloadUrl);
@@ -618,7 +625,7 @@ function isHostWhitelisted(host) {
618 return WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES.includes(host);625 return WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES.includes(host);
619}626}
620627
621const router = express.Router();628export const router = express.Router();
622629
623router.post('/importURL', jsonParser, async (request, response) => {630router.post('/importURL', jsonParser, async (request, response) => {
624 if (!request.body.url) {631 if (!request.body.url) {
@@ -753,12 +760,3 @@ router.post('/importUUID', jsonParser, async (request, response) => {
753 return response.sendStatus(500);760 return response.sendStatus(500);
754 }761 }
755});762});
756
757module.exports = {
758 CONTENT_TYPES,
759 checkForNewContent,
760 getDefaultPresets,
761 getDefaultPresetFile,
762 getContentOfType,
763 router,
764};
src/endpoints/extensions.js+10 -10
@@ -1,10 +1,12 @@
1const path = require('path');1import path from 'node:path';
2const fs = require('fs');2import fs from 'node:fs';
3const express = require('express');3
4const { default: simpleGit } = require('simple-git');4import express from 'express';
5const sanitize = require('sanitize-filename');5import sanitize from 'sanitize-filename';
6const { PUBLIC_DIRECTORIES } = require('../constants');6import { default as simpleGit } from 'simple-git';
7const { jsonParser } = require('../express-common');7
8import { PUBLIC_DIRECTORIES } from '../constants.js';
9import { jsonParser } from '../express-common.js';
810
9/**11/**
10 * This function extracts the extension information from the manifest file.12 * This function extracts the extension information from the manifest file.
@@ -47,7 +49,7 @@ async function checkIfRepoIsUpToDate(extensionPath) {
47 };49 };
48}50}
4951
50const router = express.Router();52export const router = express.Router();
5153
52/**54/**
53 * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,55 * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
@@ -240,5 +242,3 @@ router.get('/discover', jsonParser, function (request, response) {
240242
241 return response.send(extensions);243 return response.send(extensions);
242});244});
243
244module.exports = { router };
src/endpoints/files.js+12 -11
@@ -1,12 +1,15 @@
1const path = require('path');1import path from 'node:path';
2const fs = require('fs');2import fs from 'node:fs';
3const writeFileSyncAtomic = require('write-file-atomic').sync;3
4const express = require('express');4import express from 'express';
5const sanitize = require('sanitize-filename');5import sanitize from 'sanitize-filename';
6const router = express.Router();6import { sync as writeFileSyncAtomic } from 'write-file-atomic';
7const { validateAssetFileName } = require('./assets');7
8const { jsonParser } = require('../express-common');8import { validateAssetFileName } from './assets.js';
9const { clientRelativePath } = require('../util');9import { jsonParser } from '../express-common.js';
10import { clientRelativePath } from '../util.js';
11
12export const router = express.Router();
1013
11router.post('/sanitize-filename', jsonParser, async (request, response) => {14router.post('/sanitize-filename', jsonParser, async (request, response) => {
12 try {15 try {
@@ -97,5 +100,3 @@ router.post('/verify', jsonParser, async (request, response) => {
97 return response.sendStatus(500);100 return response.sendStatus(500);
98 }101 }
99});102});
100
101module.exports = { router };
src/endpoints/google.js+8 -9
@@ -1,12 +1,13 @@
1const { readSecret, SECRET_KEYS } = require('./secrets');1import fetch from 'node-fetch';
2const fetch = require('node-fetch').default;2import express from 'express';
3const express = require('express');3
4const { jsonParser } = require('../express-common');4import { readSecret, SECRET_KEYS } from './secrets.js';
5const { GEMINI_SAFETY } = require('../constants');5import { jsonParser } from '../express-common.js';
6import { GEMINI_SAFETY } from '../constants.js';
67
7const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';8const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
89
9const router = express.Router();10export const router = express.Router();
1011
11router.post('/caption-image', jsonParser, async (request, response) => {12router.post('/caption-image', jsonParser, async (request, response) => {
12 try {13 try {
@@ -39,7 +40,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
39 headers: {40 headers: {
40 'Content-Type': 'application/json',41 'Content-Type': 'application/json',
41 },42 },
42 timeout: 0,
43 });43 });
4444
45 if (!result.ok) {45 if (!result.ok) {
@@ -48,6 +48,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
48 return response.status(result.status).send({ error: true });48 return response.status(result.status).send({ error: true });
49 }49 }
5050
51 /** @type {any} */
51 const data = await result.json();52 const data = await result.json();
52 console.log('Multimodal captioning response', data);53 console.log('Multimodal captioning response', data);
5354
@@ -67,5 +68,3 @@ router.post('/caption-image', jsonParser, async (request, response) => {
67 response.status(500).send('Internal server error');68 response.status(500).send('Internal server error');
68 }69 }
69});70});
70
71module.exports = { router };
src/endpoints/groups.js+9 -10
@@ -1,13 +1,14 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');
4const sanitize = require('sanitize-filename');
5const writeFileAtomicSync = require('write-file-atomic').sync;
63
7const { jsonParser } = require('../express-common');4import express from 'express';
8const { humanizedISO8601DateTime } = require('../util');5import sanitize from 'sanitize-filename';
6import { sync as writeFileAtomicSync } from 'write-file-atomic';
97
10const router = express.Router();8import { jsonParser } from '../express-common.js';
9import { humanizedISO8601DateTime } from '../util.js';
10
11export const router = express.Router();
1112
12router.post('/all', jsonParser, (request, response) => {13router.post('/all', jsonParser, (request, response) => {
13 const groups = [];14 const groups = [];
@@ -131,5 +132,3 @@ router.post('/delete', jsonParser, async (request, response) => {
131132
132 return response.send({ ok: true });133 return response.send({ ok: true });
133});134});
134
135module.exports = { router };
src/endpoints/horde.js+7 -9
@@ -1,14 +1,14 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const express = require('express');2import express from 'express';
3const { AIHorde, ModelGenerationInputStableSamplers, ModelInterrogationFormTypes, HordeAsyncRequestStates } = require('@zeldafan0225/ai_horde');3import { AIHorde, ModelGenerationInputStableSamplers, ModelInterrogationFormTypes, HordeAsyncRequestStates } from '@zeldafan0225/ai_horde';
4const { getVersion, delay, Cache } = require('../util');4import { getVersion, delay, Cache } from '../util.js';
5const { readSecret, SECRET_KEYS } = require('./secrets');5import { readSecret, SECRET_KEYS } from './secrets.js';
6const { jsonParser } = require('../express-common');6import { jsonParser } from '../express-common.js';
77
8const ANONYMOUS_KEY = '0000000000';8const ANONYMOUS_KEY = '0000000000';
9const HORDE_TEXT_MODEL_METADATA_URL = 'https://raw.githubusercontent.com/db0/AI-Horde-text-model-reference/main/db.json';9const HORDE_TEXT_MODEL_METADATA_URL = 'https://raw.githubusercontent.com/db0/AI-Horde-text-model-reference/main/db.json';
10const cache = new Cache(60 * 1000);10const cache = new Cache(60 * 1000);
11const router = express.Router();11export const router = express.Router();
1212
13/**13/**
14 * Returns the AIHorde client agent.14 * Returns the AIHorde client agent.
@@ -403,5 +403,3 @@ router.post('/generate-image', jsonParser, async (request, response) => {
403 return response.sendStatus(500);403 return response.sendStatus(500);
404 }404 }
405});405});
406
407module.exports = { router };
src/endpoints/images.js+10 -10
@@ -1,10 +1,12 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3import { Buffer } from 'node:buffer';
4const sanitize = require('sanitize-filename');
54
6const { jsonParser } = require('../express-common');5import express from 'express';
7const { clientRelativePath, removeFileExtension, getImages } = require('../util');6import sanitize from 'sanitize-filename';
7
8import { jsonParser } from '../express-common.js';
9import { clientRelativePath, removeFileExtension, getImages } from '../util.js';
810
9/**11/**
10 * Ensure the directory for the provided file path exists.12 * Ensure the directory for the provided file path exists.
@@ -21,7 +23,7 @@ function ensureDirectoryExistence(filePath) {
21 fs.mkdirSync(dirname);23 fs.mkdirSync(dirname);
22}24}
2325
24const router = express.Router();26export const router = express.Router();
2527
26/**28/**
27 * Endpoint to handle image uploads.29 * Endpoint to handle image uploads.
@@ -66,7 +68,7 @@ router.post('/upload', jsonParser, async (request, response) => {
6668
67 ensureDirectoryExistence(pathToNewFile);69 ensureDirectoryExistence(pathToNewFile);
68 const imageBuffer = Buffer.from(base64Data, 'base64');70 const imageBuffer = Buffer.from(base64Data, 'base64');
69 await fs.promises.writeFile(pathToNewFile, imageBuffer);71 await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer));
70 response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });72 response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });
71 } catch (error) {73 } catch (error) {
72 console.log(error);74 console.log(error);
@@ -89,5 +91,3 @@ router.post('/list/:folder', (request, response) => {
89 return response.status(500).send({ error: 'Unable to retrieve files' });91 return response.status(500).send({ error: 'Unable to retrieve files' });
90 }92 }
91});93});
92
93module.exports = { router };
src/endpoints/moving-ui.js+6 -8
@@ -1,11 +1,11 @@
1const path = require('path');1import path from 'node:path';
2const express = require('express');2import express from 'express';
3const sanitize = require('sanitize-filename');3import sanitize from 'sanitize-filename';
4const writeFileAtomicSync = require('write-file-atomic').sync;4import { sync as writeFileAtomicSync } from 'write-file-atomic';
55
6const { jsonParser } = require('../express-common');6import { jsonParser } from '../express-common.js';
77
8const router = express.Router();8export const router = express.Router();
99
10router.post('/save', jsonParser, (request, response) => {10router.post('/save', jsonParser, (request, response) => {
11 if (!request.body || !request.body.name) {11 if (!request.body || !request.body.name) {
@@ -17,5 +17,3 @@ router.post('/save', jsonParser, (request, response) => {
1717
18 return response.sendStatus(200);18 return response.sendStatus(200);
19});19});
20
21module.exports = { router };
src/endpoints/novelai.js+13 -12
@@ -1,9 +1,12 @@
1const fetch = require('node-fetch').default;1import util from 'node:util';
2const express = require('express');2import { Buffer } from 'node:buffer';
3const util = require('util');3
4const { readSecret, SECRET_KEYS } = require('./secrets');4import fetch from 'node-fetch';
5const { readAllChunks, extractFileFromZipBuffer, forwardFetchResponse } = require('../util');5import express from 'express';
6const { jsonParser } = require('../express-common');6
7import { readSecret, SECRET_KEYS } from './secrets.js';
8import { readAllChunks, extractFileFromZipBuffer, forwardFetchResponse } from '../util.js';
9import { jsonParser } from '../express-common.js';
710
8const API_NOVELAI = 'https://api.novelai.net';11const API_NOVELAI = 'https://api.novelai.net';
9const TEXT_NOVELAI = 'https://text.novelai.net';12const TEXT_NOVELAI = 'https://text.novelai.net';
@@ -110,7 +113,7 @@ function getRepPenaltyWhitelist(model) {
110 return null;113 return null;
111}114}
112115
113const router = express.Router();116export const router = express.Router();
114117
115router.post('/status', jsonParser, async function (req, res) {118router.post('/status', jsonParser, async function (req, res) {
116 if (!req.body) return res.sendStatus(400);119 if (!req.body) return res.sendStatus(400);
@@ -249,7 +252,7 @@ router.post('/generate', jsonParser, async function (req, res) {
249 try {252 try {
250 const baseURL = (req.body.model.includes('kayra') || req.body.model.includes('erato')) ? TEXT_NOVELAI : API_NOVELAI;253 const baseURL = (req.body.model.includes('kayra') || req.body.model.includes('erato')) ? TEXT_NOVELAI : API_NOVELAI;
251 const url = req.body.streaming ? `${baseURL}/ai/generate-stream` : `${baseURL}/ai/generate`;254 const url = req.body.streaming ? `${baseURL}/ai/generate-stream` : `${baseURL}/ai/generate`;
252 const response = await fetch(url, { method: 'POST', timeout: 0, ...args });255 const response = await fetch(url, { method: 'POST', ...args });
253256
254 if (req.body.streaming) {257 if (req.body.streaming) {
255 // Pipe remote SSE stream to Express response258 // Pipe remote SSE stream to Express response
@@ -271,6 +274,7 @@ router.post('/generate', jsonParser, async function (req, res) {
271 return res.status(response.status).send({ error: { message } });274 return res.status(response.status).send({ error: { message } });
272 }275 }
273276
277 /** @type {any} */
274 const data = await response.json();278 const data = await response.json();
275 console.log('NovelAI Output', data?.output);279 console.log('NovelAI Output', data?.output);
276 return res.send(data);280 return res.send(data);
@@ -413,7 +417,6 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
413 'Authorization': `Bearer ${token}`,417 'Authorization': `Bearer ${token}`,
414 'Accept': 'audio/mpeg',418 'Accept': 'audio/mpeg',
415 },419 },
416 timeout: 0,
417 });420 });
418421
419 if (!result.ok) {422 if (!result.ok) {
@@ -423,7 +426,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
423 }426 }
424427
425 const chunks = await readAllChunks(result.body);428 const chunks = await readAllChunks(result.body);
426 const buffer = Buffer.concat(chunks);429 const buffer = Buffer.concat(chunks.map(chunk => new Uint8Array(chunk)));
427 response.setHeader('Content-Type', 'audio/mpeg');430 response.setHeader('Content-Type', 'audio/mpeg');
428 return response.send(buffer);431 return response.send(buffer);
429 }432 }
@@ -432,5 +435,3 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
432 return response.sendStatus(500);435 return response.sendStatus(500);
433 }436 }
434});437});
435
436module.exports = { router };
src/endpoints/openai.js+15 -15
@@ -1,14 +1,17 @@
1const { readSecret, SECRET_KEYS } = require('./secrets');1import fs from 'node:fs';
2const fetch = require('node-fetch').default;2import { Buffer } from 'node:buffer';
3const express = require('express');3
4const FormData = require('form-data');4import fetch from 'node-fetch';
5const fs = require('fs');5import FormData from 'form-data';
6const { jsonParser, urlencodedParser } = require('../express-common');6import express from 'express';
7const { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } = require('../util');7
8const { setAdditionalHeaders } = require('../additional-headers');8import { jsonParser, urlencodedParser } from '../express-common.js';
9const { OPENROUTER_HEADERS } = require('../constants');9import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } from '../util.js';
1010import { setAdditionalHeaders } from '../additional-headers.js';
11const router = express.Router();11import { readSecret, SECRET_KEYS } from './secrets.js';
12import { OPENROUTER_HEADERS } from '../constants.js';
13
14export const router = express.Router();
1215
13router.post('/caption-image', jsonParser, async (request, response) => {16router.post('/caption-image', jsonParser, async (request, response) => {
14 try {17 try {
@@ -151,7 +154,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
151 ...headers,154 ...headers,
152 },155 },
153 body: JSON.stringify(body),156 body: JSON.stringify(body),
154 timeout: 0,
155 });157 });
156158
157 if (!result.ok) {159 if (!result.ok) {
@@ -160,6 +162,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
160 return response.status(500).send(text);162 return response.status(500).send(text);
161 }163 }
162164
165 /** @type {any} */
163 const data = await result.json();166 const data = await result.json();
164 console.log('Multimodal captioning response', data);167 console.log('Multimodal captioning response', data);
165 const caption = data?.choices[0]?.message?.content;168 const caption = data?.choices[0]?.message?.content;
@@ -281,7 +284,6 @@ router.post('/generate-image', jsonParser, async (request, response) => {
281 Authorization: `Bearer ${key}`,284 Authorization: `Bearer ${key}`,
282 },285 },
283 body: JSON.stringify(request.body),286 body: JSON.stringify(request.body),
284 timeout: 0,
285 });287 });
286288
287 if (!result.ok) {289 if (!result.ok) {
@@ -341,5 +343,3 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
341});343});
342344
343router.use('/custom', custom);345router.use('/custom', custom);
344
345module.exports = { router };
src/endpoints/openrouter.js+3 -5
@@ -1,7 +1,7 @@
1const express = require('express');1import express from 'express';
2const { jsonParser } = require('../express-common');2import { jsonParser } from '../express-common.js';
33
4const router = express.Router();4export const router = express.Router();
5const API_OPENROUTER = 'https://openrouter.ai/api/v1';5const API_OPENROUTER = 'https://openrouter.ai/api/v1';
66
7router.post('/models/multimodal', jsonParser, async (_req, res) => {7router.post('/models/multimodal', jsonParser, async (_req, res) => {
@@ -28,5 +28,3 @@ router.post('/models/multimodal', jsonParser, async (_req, res) => {
28 return res.sendStatus(500);28 return res.sendStatus(500);
29 }29 }
30});30});
31
32module.exports = { router };
src/endpoints/presets.js+11 -11
@@ -1,15 +1,17 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3
4const sanitize = require('sanitize-filename');4import express from 'express';
5const writeFileAtomicSync = require('write-file-atomic').sync;5import sanitize from 'sanitize-filename';
6const { getDefaultPresetFile, getDefaultPresets } = require('./content-manager');6import { sync as writeFileAtomicSync } from 'write-file-atomic';
7const { jsonParser } = require('../express-common');7
8import { getDefaultPresetFile, getDefaultPresets } from './content-manager.js';
9import { jsonParser } from '../express-common.js';
810
9/**11/**
10 * Gets the folder and extension for the preset settings based on the API source ID.12 * Gets the folder and extension for the preset settings based on the API source ID.
11 * @param {string} apiId API source ID13 * @param {string} apiId API source ID
12 * @param {import('../users').UserDirectoryList} directories User directories14 * @param {import('../users.js').UserDirectoryList} directories User directories
13 * @returns {object} Object containing the folder and extension for the preset settings15 * @returns {object} Object containing the folder and extension for the preset settings
14 */16 */
15function getPresetSettingsByAPI(apiId, directories) {17function getPresetSettingsByAPI(apiId, directories) {
@@ -34,7 +36,7 @@ function getPresetSettingsByAPI(apiId, directories) {
34 }36 }
35}37}
3638
37const router = express.Router();39export const router = express.Router();
3840
39router.post('/save', jsonParser, function (request, response) {41router.post('/save', jsonParser, function (request, response) {
40 const name = sanitize(request.body.name);42 const name = sanitize(request.body.name);
@@ -127,5 +129,3 @@ router.post('/delete-openai', jsonParser, function (request, response) {
127129
128 return response.send({ error: true });130 return response.send({ error: true });
129});131});
130
131module.exports = { router };
src/endpoints/quick-replies.js+8 -9
@@ -1,12 +1,13 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');
4const sanitize = require('sanitize-filename');
5const writeFileAtomicSync = require('write-file-atomic').sync;
63
7const { jsonParser } = require('../express-common');4import express from 'express';
5import sanitize from 'sanitize-filename';
6import { sync as writeFileAtomicSync } from 'write-file-atomic';
87
9const router = express.Router();8import { jsonParser } from '../express-common.js';
9
10export const router = express.Router();
1011
11router.post('/save', jsonParser, (request, response) => {12router.post('/save', jsonParser, (request, response) => {
12 if (!request.body || !request.body.name) {13 if (!request.body || !request.body.name) {
@@ -31,5 +32,3 @@ router.post('/delete', jsonParser, (request, response) => {
3132
32 return response.sendStatus(200);33 return response.sendStatus(200);
33});34});
34
35module.exports = { router };
src/endpoints/search.js+8 -9
@@ -1,9 +1,11 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const express = require('express');2import express from 'express';
3const { readSecret, SECRET_KEYS } = require('./secrets');
4const { jsonParser } = require('../express-common');
53
6const router = express.Router();4import { decode } from 'html-entities';
5import { readSecret, SECRET_KEYS } from './secrets.js';
6import { jsonParser } from '../express-common.js';
7
8export const router = express.Router();
79
8// Cosplay as Chrome10// Cosplay as Chrome
9const visitHeaders = {11const visitHeaders = {
@@ -29,7 +31,6 @@ const visitHeaders = {
29 * @returns {Promise<string>} Transcript text31 * @returns {Promise<string>} Transcript text
30 */32 */
31async function extractTranscript(videoPageBody, lang) {33async function extractTranscript(videoPageBody, lang) {
32 const he = require('he');
33 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;34 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
34 const splittedHTML = videoPageBody.split('"captions":');35 const splittedHTML = videoPageBody.split('"captions":');
3536
@@ -84,7 +85,7 @@ async function extractTranscript(videoPageBody, lang) {
84 lang: lang ?? captions.captionTracks[0].languageCode,85 lang: lang ?? captions.captionTracks[0].languageCode,
85 }));86 }));
86 // The text is double-encoded87 // The text is double-encoded
87 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');88 const transcriptText = transcript.map((line) => decode(decode(line.text))).join(' ');
88 return transcriptText;89 return transcriptText;
89}90}
9091
@@ -263,5 +264,3 @@ router.post('/visit', jsonParser, async (request, response) => {
263 return response.sendStatus(500);264 return response.sendStatus(500);
264 }265 }
265});266});
266
267module.exports = { router };
src/endpoints/secrets.js+21 -30
@@ -1,12 +1,13 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3
4const { getConfigValue } = require('../util');4import express from 'express';
5const writeFileAtomicSync = require('write-file-atomic').sync;5import { sync as writeFileAtomicSync } from 'write-file-atomic';
6const { jsonParser } = require('../express-common');6import { getConfigValue } from '../util.js';
77import { jsonParser } from '../express-common.js';
8const SECRETS_FILE = 'secrets.json';8
9const SECRET_KEYS = {9export const SECRETS_FILE = 'secrets.json';
10export const SECRET_KEYS = {
10 HORDE: 'api_key_horde',11 HORDE: 'api_key_horde',
11 MANCER: 'api_key_mancer',12 MANCER: 'api_key_mancer',
12 VLLM: 'api_key_vllm',13 VLLM: 'api_key_vllm',
@@ -58,11 +59,11 @@ const EXPORTABLE_KEYS = [
5859
59/**60/**
60 * Writes a secret to the secrets file61 * Writes a secret to the secrets file
61 * @param {import('../users').UserDirectoryList} directories User directories62 * @param {import('../users.js').UserDirectoryList} directories User directories
62 * @param {string} key Secret key63 * @param {string} key Secret key
63 * @param {string} value Secret value64 * @param {string} value Secret value
64 */65 */
65function writeSecret(directories, key, value) {66export function writeSecret(directories, key, value) {
66 const filePath = path.join(directories.root, SECRETS_FILE);67 const filePath = path.join(directories.root, SECRETS_FILE);
6768
68 if (!fs.existsSync(filePath)) {69 if (!fs.existsSync(filePath)) {
@@ -78,11 +79,11 @@ function writeSecret(directories, key, value) {
7879
79/**80/**
80 * Deletes a secret from the secrets file81 * Deletes a secret from the secrets file
81 * @param {import('../users').UserDirectoryList} directories User directories82 * @param {import('../users.js').UserDirectoryList} directories User directories
82 * @param {string} key Secret key83 * @param {string} key Secret key
83 * @returns84 * @returns
84 */85 */
85function deleteSecret(directories, key) {86export function deleteSecret(directories, key) {
86 const filePath = path.join(directories.root, SECRETS_FILE);87 const filePath = path.join(directories.root, SECRETS_FILE);
8788
88 if (!fs.existsSync(filePath)) {89 if (!fs.existsSync(filePath)) {
@@ -97,11 +98,11 @@ function deleteSecret(directories, key) {
9798
98/**99/**
99 * Reads a secret from the secrets file100 * Reads a secret from the secrets file
100 * @param {import('../users').UserDirectoryList} directories User directories101 * @param {import('../users.js').UserDirectoryList} directories User directories
101 * @param {string} key Secret key102 * @param {string} key Secret key
102 * @returns {string} Secret value103 * @returns {string} Secret value
103 */104 */
104function readSecret(directories, key) {105export function readSecret(directories, key) {
105 const filePath = path.join(directories.root, SECRETS_FILE);106 const filePath = path.join(directories.root, SECRETS_FILE);
106107
107 if (!fs.existsSync(filePath)) {108 if (!fs.existsSync(filePath)) {
@@ -115,10 +116,10 @@ function readSecret(directories, key) {
115116
116/**117/**
117 * Reads the secret state from the secrets file118 * Reads the secret state from the secrets file
118 * @param {import('../users').UserDirectoryList} directories User directories119 * @param {import('../users.js').UserDirectoryList} directories User directories
119 * @returns {object} Secret state120 * @returns {object} Secret state
120 */121 */
121function readSecretState(directories) {122export function readSecretState(directories) {
122 const filePath = path.join(directories.root, SECRETS_FILE);123 const filePath = path.join(directories.root, SECRETS_FILE);
123124
124 if (!fs.existsSync(filePath)) {125 if (!fs.existsSync(filePath)) {
@@ -138,10 +139,10 @@ function readSecretState(directories) {
138139
139/**140/**
140 * Reads all secrets from the secrets file141 * Reads all secrets from the secrets file
141 * @param {import('../users').UserDirectoryList} directories User directories142 * @param {import('../users.js').UserDirectoryList} directories User directories
142 * @returns {Record<string, string> | undefined} Secrets143 * @returns {Record<string, string> | undefined} Secrets
143 */144 */
144function getAllSecrets(directories) {145export function getAllSecrets(directories) {
145 const filePath = path.join(directories.root, SECRETS_FILE);146 const filePath = path.join(directories.root, SECRETS_FILE);
146147
147 if (!fs.existsSync(filePath)) {148 if (!fs.existsSync(filePath)) {
@@ -154,7 +155,7 @@ function getAllSecrets(directories) {
154 return secrets;155 return secrets;
155}156}
156157
157const router = express.Router();158export const router = express.Router();
158159
159router.post('/write', jsonParser, (request, response) => {160router.post('/write', jsonParser, (request, response) => {
160 const key = request.body.key;161 const key = request.body.key;
@@ -218,13 +219,3 @@ router.post('/find', jsonParser, (request, response) => {
218 return response.sendStatus(500);219 return response.sendStatus(500);
219 }220 }
220});221});
221
222module.exports = {
223 writeSecret,
224 readSecret,
225 deleteSecret,
226 readSecretState,
227 getAllSecrets,
228 SECRET_KEYS,
229 router,
230};
src/endpoints/settings.js+13 -13
@@ -1,12 +1,14 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3
4const _ = require('lodash');4import express from 'express';
5const writeFileAtomicSync = require('write-file-atomic').sync;5import _ from 'lodash';
6const { SETTINGS_FILE } = require('../constants');6import { sync as writeFileAtomicSync } from 'write-file-atomic';
7const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');7
8const { jsonParser } = require('../express-common');8import { SETTINGS_FILE } from '../constants.js';
9const { getAllUserHandles, getUserDirectories } = require('../users');9import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
10import { jsonParser } from '../express-common.js';
11import { getAllUserHandles, getUserDirectories } from '../users.js';
1012
11const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);13const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
12const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true);14const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true);
@@ -190,7 +192,7 @@ function getLatestBackup(handle) {
190 return path.join(userDirectories.backups, latestBackup);192 return path.join(userDirectories.backups, latestBackup);
191}193}
192194
193const router = express.Router();195export const router = express.Router();
194196
195router.post('/save', jsonParser, function (request, response) {197router.post('/save', jsonParser, function (request, response) {
196 try {198 try {
@@ -357,8 +359,6 @@ router.post('/restore-snapshot', jsonParser, async (request, response) => {
357/**359/**
358 * Initializes the settings endpoint360 * Initializes the settings endpoint
359 */361 */
360async function init() {362export async function init() {
361 await backupSettings();363 await backupSettings();
362}364}
363
364module.exports = { router, init };
src/endpoints/speech.js+8 -11
@@ -1,7 +1,10 @@
1const express = require('express');1import { Buffer } from 'node:buffer';
2const { jsonParser } = require('../express-common');2import express from 'express';
3import wavefile from 'wavefile';
4import { jsonParser } from '../express-common.js';
5import { getPipeline } from '../transformers.mjs';
36
4const router = express.Router();7export const router = express.Router();
58
6/**9/**
7 * Gets the audio data from a base64-encoded audio file.10 * Gets the audio data from a base64-encoded audio file.
@@ -9,7 +12,6 @@ const router = express.Router();
9 * @returns {Float64Array} Audio data12 * @returns {Float64Array} Audio data
10 */13 */
11function getWaveFile(audio) {14function getWaveFile(audio) {
12 const wavefile = require('wavefile');
13 const wav = new wavefile.WaveFile();15 const wav = new wavefile.WaveFile();
14 wav.fromDataURI(audio);16 wav.fromDataURI(audio);
15 wav.toBitDepth('32f');17 wav.toBitDepth('32f');
@@ -36,8 +38,7 @@ router.post('/recognize', jsonParser, async (req, res) => {
36 try {38 try {
37 const TASK = 'automatic-speech-recognition';39 const TASK = 'automatic-speech-recognition';
38 const { model, audio, lang } = req.body;40 const { model, audio, lang } = req.body;
39 const module = await import('../transformers.mjs');41 const pipe = await getPipeline(TASK, model);
40 const pipe = await module.default.getPipeline(TASK, model);
41 const wav = getWaveFile(audio);42 const wav = getWaveFile(audio);
42 const start = performance.now();43 const start = performance.now();
43 const result = await pipe(wav, { language: lang || null, task: 'transcribe' });44 const result = await pipe(wav, { language: lang || null, task: 'transcribe' });
@@ -54,11 +55,9 @@ router.post('/recognize', jsonParser, async (req, res) => {
5455
55router.post('/synthesize', jsonParser, async (req, res) => {56router.post('/synthesize', jsonParser, async (req, res) => {
56 try {57 try {
57 const wavefile = require('wavefile');
58 const TASK = 'text-to-speech';58 const TASK = 'text-to-speech';
59 const { text, model, speaker } = req.body;59 const { text, model, speaker } = req.body;
60 const module = await import('../transformers.mjs');60 const pipe = await getPipeline(TASK, model);
61 const pipe = await module.default.getPipeline(TASK, model);
62 const speaker_embeddings = speaker61 const speaker_embeddings = speaker
63 ? new Float32Array(new Uint8Array(Buffer.from(speaker.startsWith('data:') ? speaker.split(',')[1] : speaker, 'base64')).buffer)62 ? new Float32Array(new Uint8Array(Buffer.from(speaker.startsWith('data:') ? speaker.split(',')[1] : speaker, 'base64')).buffer)
64 : null;63 : null;
@@ -78,5 +77,3 @@ router.post('/synthesize', jsonParser, async (req, res) => {
78 return res.sendStatus(500);77 return res.sendStatus(500);
79 }78 }
80});79});
81
82module.exports = { router };
src/endpoints/sprites.js+13 -17
@@ -1,16 +1,17 @@
1import fs from 'node:fs';
2import path from 'node:path';
13
2const fs = require('fs');4import express from 'express';
3const path = require('path');5import mime from 'mime-types';
4const express = require('express');6import sanitize from 'sanitize-filename';
5const mime = require('mime-types');7import { sync as writeFileAtomicSync } from 'write-file-atomic';
6const sanitize = require('sanitize-filename');8
7const writeFileAtomicSync = require('write-file-atomic').sync;9import { getImageBuffers } from '../util.js';
8const { getImageBuffers } = require('../util');10import { jsonParser, urlencodedParser } from '../express-common.js';
9const { jsonParser, urlencodedParser } = require('../express-common');
1011
11/**12/**
12 * Gets the path to the sprites folder for the provided character name13 * Gets the path to the sprites folder for the provided character name
13 * @param {import('../users').UserDirectoryList} directories - User directories14 * @param {import('../users.js').UserDirectoryList} directories - User directories
14 * @param {string} name - The name of the character15 * @param {string} name - The name of the character
15 * @param {boolean} isSubfolder - Whether the name contains a subfolder16 * @param {boolean} isSubfolder - Whether the name contains a subfolder
16 * @returns {string | null} The path to the sprites folder. Null if the name is invalid.17 * @returns {string | null} The path to the sprites folder. Null if the name is invalid.
@@ -41,11 +42,11 @@ function getSpritesPath(directories, name, isSubfolder) {
41 * Imports base64 encoded sprites from RisuAI character data.42 * Imports base64 encoded sprites from RisuAI character data.
42 * The sprites are saved in the character's sprites folder.43 * The sprites are saved in the character's sprites folder.
43 * The additionalAssets and emotions are removed from the data.44 * The additionalAssets and emotions are removed from the data.
44 * @param {import('../users').UserDirectoryList} directories User directories45 * @param {import('../users.js').UserDirectoryList} directories User directories
45 * @param {object} data RisuAI character data46 * @param {object} data RisuAI character data
46 * @returns {void}47 * @returns {void}
47 */48 */
48function importRisuSprites(directories, data) {49export function importRisuSprites(directories, data) {
49 try {50 try {
50 const name = data?.data?.name;51 const name = data?.data?.name;
51 const risuData = data?.data?.extensions?.risuai;52 const risuData = data?.data?.extensions?.risuai;
@@ -106,7 +107,7 @@ function importRisuSprites(directories, data) {
106 }107 }
107}108}
108109
109const router = express.Router();110export const router = express.Router();
110111
111router.get('/get', jsonParser, function (request, response) {112router.get('/get', jsonParser, function (request, response) {
112 const name = String(request.query.name);113 const name = String(request.query.name);
@@ -259,8 +260,3 @@ router.post('/upload', urlencodedParser, async (request, response) => {
259 return response.sendStatus(500);260 return response.sendStatus(500);
260 }261 }
261});262});
262
263module.exports = {
264 router,
265 importRisuSprites,
266};
src/endpoints/stable-diffusion.js+33 -19
@@ -1,13 +1,15 @@
1const express = require('express');1import fs from 'node:fs';
2const fetch = require('node-fetch').default;2import path from 'node:path';
3const sanitize = require('sanitize-filename');3
4const { getBasicAuthHeader, delay, getHexString } = require('../util.js');4import express from 'express';
5const fs = require('fs');5import fetch from 'node-fetch';
6const path = require('path');6import sanitize from 'sanitize-filename';
7const writeFileAtomicSync = require('write-file-atomic').sync;7import { sync as writeFileAtomicSync } from 'write-file-atomic';
8const { jsonParser } = require('../express-common');8import FormData from 'form-data';
9const { readSecret, SECRET_KEYS } = require('./secrets.js');9
10const FormData = require('form-data');10import { getBasicAuthHeader, delay } from '../util.js';
11import { jsonParser } from '../express-common.js';
12import { readSecret, SECRET_KEYS } from './secrets.js';
1113
12/**14/**
13 * Gets the comfy workflows.15 * Gets the comfy workflows.
@@ -21,7 +23,7 @@ function getComfyWorkflows(directories) {
21 .sort(Intl.Collator().compare);23 .sort(Intl.Collator().compare);
22}24}
2325
24const router = express.Router();26export const router = express.Router();
2527
26router.post('/ping', jsonParser, async (request, response) => {28router.post('/ping', jsonParser, async (request, response) => {
27 try {29 try {
@@ -63,6 +65,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
63 throw new Error('SD WebUI returned an error.');65 throw new Error('SD WebUI returned an error.');
64 }66 }
6567
68 /** @type {any} */
66 const data = await result.json();69 const data = await result.json();
67 const names = data.map(x => x.name);70 const names = data.map(x => x.name);
68 return names;71 return names;
@@ -83,6 +86,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
83 throw new Error('SD WebUI returned an error.');86 throw new Error('SD WebUI returned an error.');
84 }87 }
8588
89 /** @type {any} */
86 const data = await result.json();90 const data = await result.json();
87 const names = data.map(x => x.name);91 const names = data.map(x => x.name);
88 return names;92 return names;
@@ -116,6 +120,7 @@ router.post('/vaes', jsonParser, async (request, response) => {
116 throw new Error('SD WebUI returned an error.');120 throw new Error('SD WebUI returned an error.');
117 }121 }
118122
123 /** @type {any} */
119 const data = await result.json();124 const data = await result.json();
120 const names = data.map(x => x.model_name);125 const names = data.map(x => x.model_name);
121 return response.send(names);126 return response.send(names);
@@ -141,6 +146,7 @@ router.post('/samplers', jsonParser, async (request, response) => {
141 throw new Error('SD WebUI returned an error.');146 throw new Error('SD WebUI returned an error.');
142 }147 }
143148
149 /** @type {any} */
144 const data = await result.json();150 const data = await result.json();
145 const names = data.map(x => x.name);151 const names = data.map(x => x.name);
146 return response.send(names);152 return response.send(names);
@@ -167,6 +173,7 @@ router.post('/schedulers', jsonParser, async (request, response) => {
167 throw new Error('SD WebUI returned an error.');173 throw new Error('SD WebUI returned an error.');
168 }174 }
169175
176 /** @type {any} */
170 const data = await result.json();177 const data = await result.json();
171 const names = data.map(x => x.name);178 const names = data.map(x => x.name);
172 return response.send(names);179 return response.send(names);
@@ -192,6 +199,7 @@ router.post('/models', jsonParser, async (request, response) => {
192 throw new Error('SD WebUI returned an error.');199 throw new Error('SD WebUI returned an error.');
193 }200 }
194201
202 /** @type {any} */
195 const data = await result.json();203 const data = await result.json();
196 const models = data.map(x => ({ value: x.title, text: x.title }));204 const models = data.map(x => ({ value: x.title, text: x.title }));
197 return response.send(models);205 return response.send(models);
@@ -212,6 +220,7 @@ router.post('/get-model', jsonParser, async (request, response) => {
212 'Authorization': getBasicAuthHeader(request.body.auth),220 'Authorization': getBasicAuthHeader(request.body.auth),
213 },221 },
214 });222 });
223 /** @type {any} */
215 const data = await result.json();224 const data = await result.json();
216 return response.send(data['sd_model_checkpoint']);225 return response.send(data['sd_model_checkpoint']);
217 } catch (error) {226 } catch (error) {
@@ -231,7 +240,6 @@ router.post('/set-model', jsonParser, async (request, response) => {
231 headers: {240 headers: {
232 'Authorization': getBasicAuthHeader(request.body.auth),241 'Authorization': getBasicAuthHeader(request.body.auth),
233 },242 },
234 timeout: 0,
235 });243 });
236 const data = await result.json();244 const data = await result.json();
237 return data;245 return data;
@@ -251,7 +259,6 @@ router.post('/set-model', jsonParser, async (request, response) => {
251 'Content-Type': 'application/json',259 'Content-Type': 'application/json',
252 'Authorization': getBasicAuthHeader(request.body.auth),260 'Authorization': getBasicAuthHeader(request.body.auth),
253 },261 },
254 timeout: 0,
255 });262 });
256263
257 if (!result.ok) {264 if (!result.ok) {
@@ -262,6 +269,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
262 const CHECK_INTERVAL = 2000;269 const CHECK_INTERVAL = 2000;
263270
264 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {271 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
272 /** @type {any} */
265 const progressState = await getProgress();273 const progressState = await getProgress();
266274
267 const progress = progressState['progress'];275 const progress = progressState['progress'];
@@ -306,8 +314,6 @@ router.post('/generate', jsonParser, async (request, response) => {
306 'Content-Type': 'application/json',314 'Content-Type': 'application/json',
307 'Authorization': getBasicAuthHeader(request.body.auth),315 'Authorization': getBasicAuthHeader(request.body.auth),
308 },316 },
309 timeout: 0,
310 // @ts-ignore
311 signal: controller.signal,317 signal: controller.signal,
312 });318 });
313319
@@ -343,6 +349,7 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => {
343 // Vlad doesn't provide Latent Upscalers in the API, so we have to hardcode them here349 // Vlad doesn't provide Latent Upscalers in the API, so we have to hardcode them here
344 const latentUpscalers = ['Latent', 'Latent (antialiased)', 'Latent (bicubic)', 'Latent (bicubic antialiased)', 'Latent (nearest)', 'Latent (nearest-exact)'];350 const latentUpscalers = ['Latent', 'Latent (antialiased)', 'Latent (bicubic)', 'Latent (bicubic antialiased)', 'Latent (nearest)', 'Latent (nearest-exact)'];
345351
352 /** @type {any} */
346 const data = await result.json();353 const data = await result.json();
347 const names = data.map(x => x.name);354 const names = data.map(x => x.name);
348355
@@ -385,6 +392,7 @@ comfy.post('/samplers', jsonParser, async (request, response) => {
385 throw new Error('ComfyUI returned an error.');392 throw new Error('ComfyUI returned an error.');
386 }393 }
387394
395 /** @type {any} */
388 const data = await result.json();396 const data = await result.json();
389 return response.send(data.KSampler.input.required.sampler_name[0]);397 return response.send(data.KSampler.input.required.sampler_name[0]);
390 } catch (error) {398 } catch (error) {
@@ -402,6 +410,7 @@ comfy.post('/models', jsonParser, async (request, response) => {
402 if (!result.ok) {410 if (!result.ok) {
403 throw new Error('ComfyUI returned an error.');411 throw new Error('ComfyUI returned an error.');
404 }412 }
413 /** @type {any} */
405 const data = await result.json();414 const data = await result.json();
406 return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })));415 return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })));
407 } catch (error) {416 } catch (error) {
@@ -420,6 +429,7 @@ comfy.post('/schedulers', jsonParser, async (request, response) => {
420 throw new Error('ComfyUI returned an error.');429 throw new Error('ComfyUI returned an error.');
421 }430 }
422431
432 /** @type {any} */
423 const data = await result.json();433 const data = await result.json();
424 return response.send(data.KSampler.input.required.scheduler[0]);434 return response.send(data.KSampler.input.required.scheduler[0]);
425 } catch (error) {435 } catch (error) {
@@ -438,6 +448,7 @@ comfy.post('/vaes', jsonParser, async (request, response) => {
438 throw new Error('ComfyUI returned an error.');448 throw new Error('ComfyUI returned an error.');
439 }449 }
440450
451 /** @type {any} */
441 const data = await result.json();452 const data = await result.json();
442 return response.send(data.VAELoader.input.required.vae_name[0]);453 return response.send(data.VAELoader.input.required.vae_name[0]);
443 } catch (error) {454 } catch (error) {
@@ -519,6 +530,7 @@ comfy.post('/generate', jsonParser, async (request, response) => {
519 throw new Error('ComfyUI returned an error.');530 throw new Error('ComfyUI returned an error.');
520 }531 }
521532
533 /** @type {any} */
522 const data = await promptResult.json();534 const data = await promptResult.json();
523 const id = data.prompt_id;535 const id = data.prompt_id;
524 let item;536 let item;
@@ -529,6 +541,7 @@ comfy.post('/generate', jsonParser, async (request, response) => {
529 if (!result.ok) {541 if (!result.ok) {
530 throw new Error('ComfyUI returned an error.');542 throw new Error('ComfyUI returned an error.');
531 }543 }
544 /** @type {any} */
532 const history = await result.json();545 const history = await result.json();
533 item = history[id];546 item = history[id];
534 if (item) {547 if (item) {
@@ -631,6 +644,7 @@ together.post('/generate', jsonParser, async (request, response) => {
631 return response.sendStatus(500);644 return response.sendStatus(500);
632 }645 }
633646
647 /** @type {any} */
634 const data = await result.json();648 const data = await result.json();
635 console.log('TogetherAI response:', data);649 console.log('TogetherAI response:', data);
636650
@@ -679,6 +693,8 @@ drawthings.post('/get-model', jsonParser, async (request, response) => {
679 const result = await fetch(url, {693 const result = await fetch(url, {
680 method: 'GET',694 method: 'GET',
681 });695 });
696
697 /** @type {any} */
682 const data = await result.json();698 const data = await result.json();
683699
684 return response.send(data['model']);700 return response.send(data['model']);
@@ -696,6 +712,8 @@ drawthings.post('/get-upscaler', jsonParser, async (request, response) => {
696 const result = await fetch(url, {712 const result = await fetch(url, {
697 method: 'GET',713 method: 'GET',
698 });714 });
715
716 /** @type {any} */
699 const data = await result.json();717 const data = await result.json();
700718
701 return response.send(data['upscaler']);719 return response.send(data['upscaler']);
@@ -724,7 +742,6 @@ drawthings.post('/generate', jsonParser, async (request, response) => {
724 'Content-Type': 'application/json',742 'Content-Type': 'application/json',
725 'Authorization': auth,743 'Authorization': auth,
726 },744 },
727 timeout: 0,
728 });745 });
729746
730 if (!result.ok) {747 if (!result.ok) {
@@ -846,7 +863,6 @@ stability.post('/generate', jsonParser, async (request, response) => {
846 'Accept': 'image/*',863 'Accept': 'image/*',
847 },864 },
848 body: formData,865 body: formData,
849 timeout: 0,
850 });866 });
851867
852 if (!result.ok) {868 if (!result.ok) {
@@ -993,5 +1009,3 @@ router.use('/pollinations', pollinations);
993router.use('/stability', stability);1009router.use('/stability', stability);
994router.use('/blockentropy', blockentropy);1010router.use('/blockentropy', blockentropy);
995router.use('/huggingface', huggingface);1011router.use('/huggingface', huggingface);
996
997module.exports = { router };
src/endpoints/stats.js+12 -18
@@ -1,14 +1,15 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3import crypto from 'node:crypto';
4const writeFileAtomic = require('write-file-atomic');4
5const crypto = require('crypto');5import express from 'express';
6import writeFileAtomic from 'write-file-atomic';
67
7const readFile = fs.promises.readFile;8const readFile = fs.promises.readFile;
8const readdir = fs.promises.readdir;9const readdir = fs.promises.readdir;
910
10const { jsonParser } = require('../express-common');11import { jsonParser } from '../express-common.js';
11const { getAllUserHandles, getUserDirectories } = require('../users');12import { getAllUserHandles, getUserDirectories } from '../users.js';
1213
13const STATS_FILE = 'stats.json';14const STATS_FILE = 'stats.json';
1415
@@ -146,7 +147,7 @@ async function collectAndCreateStats(chatsPath, charactersPath) {
146 * @param {string} chatsPath Path to the directory containing the chat files.147 * @param {string} chatsPath Path to the directory containing the chat files.
147 * @param {string} charactersPath Path to the directory containing the character files.148 * @param {string} charactersPath Path to the directory containing the character files.
148 */149 */
149async function recreateStats(handle, chatsPath, charactersPath) {150export async function recreateStats(handle, chatsPath, charactersPath) {
150 console.log('Collecting and creating stats for user:', handle);151 console.log('Collecting and creating stats for user:', handle);
151 const stats = await collectAndCreateStats(chatsPath, charactersPath);152 const stats = await collectAndCreateStats(chatsPath, charactersPath);
152 STATS.set(handle, stats);153 STATS.set(handle, stats);
@@ -157,7 +158,7 @@ async function recreateStats(handle, chatsPath, charactersPath) {
157 * Loads the stats file into memory. If the file doesn't exist or is invalid,158 * Loads the stats file into memory. If the file doesn't exist or is invalid,
158 * initializes stats by collecting and creating them for each character.159 * initializes stats by collecting and creating them for each character.
159 */160 */
160async function init() {161export async function init() {
161 try {162 try {
162 const userHandles = await getAllUserHandles();163 const userHandles = await getAllUserHandles();
163 for (const handle of userHandles) {164 for (const handle of userHandles) {
@@ -209,7 +210,7 @@ async function saveStatsToFile() {
209 * Attempts to save charStats to a file and then terminates the process.210 * Attempts to save charStats to a file and then terminates the process.
210 * If an error occurs during the file write, it logs the error before exiting.211 * If an error occurs during the file write, it logs the error before exiting.
211 */212 */
212async function onExit() {213export async function onExit() {
213 try {214 try {
214 await saveStatsToFile();215 await saveStatsToFile();
215 } catch (err) {216 } catch (err) {
@@ -434,7 +435,7 @@ function calculateTotalGenTimeAndWordCount(
434 };435 };
435}436}
436437
437const router = express.Router();438export const router = express.Router();
438439
439/**440/**
440 * Handle a POST request to get the stats object441 * Handle a POST request to get the stats object
@@ -465,10 +466,3 @@ router.post('/update', jsonParser, function (request, response) {
465 setCharStats(request.user.profile.handle, request.body);466 setCharStats(request.user.profile.handle, request.body);
466 return response.sendStatus(200);467 return response.sendStatus(200);
467});468});
468
469module.exports = {
470 router,
471 recreateStats,
472 init,
473 onExit,
474};
src/endpoints/themes.js+9 -9
@@ -1,11 +1,13 @@
1const express = require('express');1import path from 'node:path';
2const path = require('path');2import fs from 'node:fs';
3const fs = require('fs');
4const sanitize = require('sanitize-filename');
5const writeFileAtomicSync = require('write-file-atomic').sync;
6const { jsonParser } = require('../express-common');
73
8const router = express.Router();4import express from 'express';
5import sanitize from 'sanitize-filename';
6import { sync as writeFileAtomicSync } from 'write-file-atomic';
7
8import { jsonParser } from '../express-common.js';
9
10export const router = express.Router();
911
10router.post('/save', jsonParser, (request, response) => {12router.post('/save', jsonParser, (request, response) => {
11 if (!request.body || !request.body.name) {13 if (!request.body || !request.body.name) {
@@ -36,5 +38,3 @@ router.post('/delete', jsonParser, function (request, response) {
36 return response.sendStatus(500);38 return response.sendStatus(500);
37 }39 }
38});40});
39
40module.exports = { router };
src/endpoints/thumbnails.js+20 -24
@@ -1,14 +1,16 @@
1const fs = require('fs');1import fs from 'node:fs';
2const fsPromises = require('fs').promises;2import { promises as fsPromises } from 'node:fs';
3const path = require('path');3import path from 'node:path';
4const mime = require('mime-types');4
5const express = require('express');5import mime from 'mime-types';
6const sanitize = require('sanitize-filename');6import express from 'express';
7const jimp = require('jimp');7import sanitize from 'sanitize-filename';
8const writeFileAtomicSync = require('write-file-atomic').sync;8import jimp from 'jimp';
9const { getAllUserHandles, getUserDirectories } = require('../users');9import { sync as writeFileAtomicSync } from 'write-file-atomic';
10const { getConfigValue } = require('../util');10
11const { jsonParser } = require('../express-common');11import { getAllUserHandles, getUserDirectories } from '../users.js';
12import { getConfigValue } from '../util.js';
13import { jsonParser } from '../express-common.js';
1214
13const thumbnailsDisabled = getConfigValue('disableThumbnails', false);15const thumbnailsDisabled = getConfigValue('disableThumbnails', false);
14const quality = getConfigValue('thumbnailsQuality', 95);16const quality = getConfigValue('thumbnailsQuality', 95);
@@ -16,7 +18,7 @@ const pngFormat = getConfigValue('avatarThumbnailsPng', false);
1618
17/**19/**
18 * Gets a path to thumbnail folder based on the type.20 * Gets a path to thumbnail folder based on the type.
19 * @param {import('../users').UserDirectoryList} directories User directories21 * @param {import('../users.js').UserDirectoryList} directories User directories
20 * @param {'bg' | 'avatar'} type Thumbnail type22 * @param {'bg' | 'avatar'} type Thumbnail type
21 * @returns {string} Path to the thumbnails folder23 * @returns {string} Path to the thumbnails folder
22 */24 */
@@ -37,7 +39,7 @@ function getThumbnailFolder(directories, type) {
3739
38/**40/**
39 * Gets a path to the original images folder based on the type.41 * Gets a path to the original images folder based on the type.
40 * @param {import('../users').UserDirectoryList} directories User directories42 * @param {import('../users.js').UserDirectoryList} directories User directories
41 * @param {'bg' | 'avatar'} type Thumbnail type43 * @param {'bg' | 'avatar'} type Thumbnail type
42 * @returns {string} Path to the original images folder44 * @returns {string} Path to the original images folder
43 */45 */
@@ -58,11 +60,11 @@ function getOriginalFolder(directories, type) {
5860
59/**61/**
60 * Removes the generated thumbnail from the disk.62 * Removes the generated thumbnail from the disk.
61 * @param {import('../users').UserDirectoryList} directories User directories63 * @param {import('../users.js').UserDirectoryList} directories User directories
62 * @param {'bg' | 'avatar'} type Type of the thumbnail64 * @param {'bg' | 'avatar'} type Type of the thumbnail
63 * @param {string} file Name of the file65 * @param {string} file Name of the file
64 */66 */
65function invalidateThumbnail(directories, type, file) {67export function invalidateThumbnail(directories, type, file) {
66 const folder = getThumbnailFolder(directories, type);68 const folder = getThumbnailFolder(directories, type);
67 if (folder === undefined) throw new Error('Invalid thumbnail type');69 if (folder === undefined) throw new Error('Invalid thumbnail type');
6870
@@ -75,7 +77,7 @@ function invalidateThumbnail(directories, type, file) {
7577
76/**78/**
77 * Generates a thumbnail for the given file.79 * Generates a thumbnail for the given file.
78 * @param {import('../users').UserDirectoryList} directories User directories80 * @param {import('../users.js').UserDirectoryList} directories User directories
79 * @param {'bg' | 'avatar'} type Type of the thumbnail81 * @param {'bg' | 'avatar'} type Type of the thumbnail
80 * @param {string} file Name of the file82 * @param {string} file Name of the file
81 * @returns83 * @returns
@@ -141,7 +143,7 @@ async function generateThumbnail(directories, type, file) {
141 * Ensures that the thumbnail cache for backgrounds is valid.143 * Ensures that the thumbnail cache for backgrounds is valid.
142 * @returns {Promise<void>} Promise that resolves when the cache is validated144 * @returns {Promise<void>} Promise that resolves when the cache is validated
143 */145 */
144async function ensureThumbnailCache() {146export async function ensureThumbnailCache() {
145 const userHandles = await getAllUserHandles();147 const userHandles = await getAllUserHandles();
146 for (const handle of userHandles) {148 for (const handle of userHandles) {
147 const directories = getUserDirectories(handle);149 const directories = getUserDirectories(handle);
@@ -166,7 +168,7 @@ async function ensureThumbnailCache() {
166 }168 }
167}169}
168170
169const router = express.Router();171export const router = express.Router();
170172
171// Important: This route must be mounted as '/thumbnail'. It is used in the client code and saved to chat files.173// Important: This route must be mounted as '/thumbnail'. It is used in the client code and saved to chat files.
172router.get('/', jsonParser, async function (request, response) {174router.get('/', jsonParser, async function (request, response) {
@@ -227,9 +229,3 @@ router.get('/', jsonParser, async function (request, response) {
227 return response.sendStatus(500);229 return response.sendStatus(500);
228 }230 }
229});231});
230
231module.exports = {
232 invalidateThumbnail,
233 ensureThumbnailCache,
234 router,
235};
src/endpoints/tokenizers.js+23 -29
@@ -1,15 +1,19 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');3import { Buffer } from 'node:buffer';
4const { SentencePieceProcessor } = require('@agnai/sentencepiece-js');4
5const tiktoken = require('tiktoken');5import express from 'express';
6const { Tokenizer } = require('@agnai/web-tokenizers');6import { sync as writeFileAtomicSync } from 'write-file-atomic';
7const { convertClaudePrompt } = require('../prompt-converters');7
8const { TEXTGEN_TYPES } = require('../constants');8import { Tokenizer } from '@agnai/web-tokenizers';
9const { jsonParser } = require('../express-common');9import { SentencePieceProcessor } from '@agnai/sentencepiece-js';
10const { setAdditionalHeaders } = require('../additional-headers');10import tiktoken from 'tiktoken';
11const { getConfigValue, isValidUrl } = require('../util');11
12const writeFileAtomicSync = require('write-file-atomic').sync;12import { convertClaudePrompt } from '../prompt-converters.js';
13import { TEXTGEN_TYPES } from '../constants.js';
14import { jsonParser } from '../express-common.js';
15import { setAdditionalHeaders } from '../additional-headers.js';
16import { getConfigValue, isValidUrl } from '../util.js';
1317
14/**18/**
15 * @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler19 * @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler
@@ -23,7 +27,7 @@ const tokenizersCache = {};
23/**27/**
24 * @type {string[]}28 * @type {string[]}
25 */29 */
26const TEXT_COMPLETION_MODELS = [30export const TEXT_COMPLETION_MODELS = [
27 'gpt-3.5-turbo-instruct',31 'gpt-3.5-turbo-instruct',
28 'gpt-3.5-turbo-instruct-0914',32 'gpt-3.5-turbo-instruct-0914',
29 'text-davinci-003',33 'text-davinci-003',
@@ -223,7 +227,7 @@ const commandTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyT
223const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json', 'src/tokenizers/llama3.json');227const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json', 'src/tokenizers/llama3.json');
224const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json', 'src/tokenizers/llama3.json');228const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json', 'src/tokenizers/llama3.json');
225229
226const sentencepieceTokenizers = [230export const sentencepieceTokenizers = [
227 'llama',231 'llama',
228 'nerdstash',232 'nerdstash',
229 'nerdstash_v2',233 'nerdstash_v2',
@@ -238,7 +242,7 @@ const sentencepieceTokenizers = [
238 * @param {string} model Sentencepiece model name242 * @param {string} model Sentencepiece model name
239 * @returns {SentencePieceTokenizer|null} Sentencepiece tokenizer243 * @returns {SentencePieceTokenizer|null} Sentencepiece tokenizer
240 */244 */
241function getSentencepiceTokenizer(model) {245export function getSentencepiceTokenizer(model) {
242 if (model.includes('llama')) {246 if (model.includes('llama')) {
243 return spp_llama;247 return spp_llama;
244 }248 }
@@ -350,7 +354,7 @@ function getWebTokenizersChunks(tokenizer, ids) {
350 * @param {string} requestModel Models to use for tokenization354 * @param {string} requestModel Models to use for tokenization
351 * @returns {string} Tokenizer model to use355 * @returns {string} Tokenizer model to use
352 */356 */
353function getTokenizerModel(requestModel) {357export function getTokenizerModel(requestModel) {
354 if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini')) {358 if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini')) {
355 return 'gpt-4o';359 return 'gpt-4o';
356 }360 }
@@ -427,7 +431,7 @@ function getTokenizerModel(requestModel) {
427 return 'gpt-3.5-turbo';431 return 'gpt-3.5-turbo';
428}432}
429433
430function getTiktokenTokenizer(model) {434export function getTiktokenTokenizer(model) {
431 if (tokenizersCache[model]) {435 if (tokenizersCache[model]) {
432 return tokenizersCache[model];436 return tokenizersCache[model];
433 }437 }
@@ -444,7 +448,7 @@ function getTiktokenTokenizer(model) {
444 * @param {object[]} messages Array of messages448 * @param {object[]} messages Array of messages
445 * @returns {number} Number of tokens449 * @returns {number} Number of tokens
446 */450 */
447function countWebTokenizerTokens(tokenizer, messages) {451export function countWebTokenizerTokens(tokenizer, messages) {
448 // Should be fine if we use the old conversion method instead of the messages API one i think?452 // Should be fine if we use the old conversion method instead of the messages API one i think?
449 const convertedPrompt = convertClaudePrompt(messages, false, '', false, false, '', false);453 const convertedPrompt = convertClaudePrompt(messages, false, '', false, false, '', false);
450454
@@ -636,7 +640,7 @@ function createWebTokenizerDecodingHandler(tokenizer) {
636 };640 };
637}641}
638642
639const router = express.Router();643export const router = express.Router();
640644
641router.post('/llama/encode', jsonParser, createSentencepieceEncodingHandler(spp_llama));645router.post('/llama/encode', jsonParser, createSentencepieceEncodingHandler(spp_llama));
642router.post('/nerdstash/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd));646router.post('/nerdstash/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd));
@@ -1002,13 +1006,3 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
1002 return response.send({ error: true });1006 return response.send({ error: true });
1003 }1007 }
1004});1008});
1005
1006module.exports = {
1007 TEXT_COMPLETION_MODELS,
1008 getTokenizerModel,
1009 getTiktokenTokenizer,
1010 countWebTokenizerTokens,
1011 getSentencepiceTokenizer,
1012 sentencepieceTokenizers,
1013 router,
1014};
src/endpoints/translate.js+21 -54
@@ -1,29 +1,18 @@
1const fetch = require('node-fetch').default;1import https from 'node:https';
2const https = require('https');2
3const express = require('express');3import fetch from 'node-fetch';
4const iconv = require('iconv-lite');4import express from 'express';
5const { readSecret, SECRET_KEYS } = require('./secrets');5import bingTranslateApi from 'bing-translate-api';
6const { getConfigValue, uuidv4 } = require('../util');6import googleTranslateApi from 'google-translate-api-x';
7const { jsonParser } = require('../express-common');7
8import { readSecret, SECRET_KEYS } from './secrets.js';
9import { getConfigValue, uuidv4 } from '../util.js';
10import { jsonParser } from '../express-common.js';
811
9const DEEPLX_URL_DEFAULT = 'http://127.0.0.1:1188/translate';12const DEEPLX_URL_DEFAULT = 'http://127.0.0.1:1188/translate';
10const ONERING_URL_DEFAULT = 'http://127.0.0.1:4990/translate';13const ONERING_URL_DEFAULT = 'http://127.0.0.1:4990/translate';
1114
12/**15export const router = express.Router();
13 * Tries to decode a Node.js Buffer to a string using iconv-lite for UTF-8.
14 * @param {Buffer} buffer Node.js Buffer
15 * @returns {string} Decoded string
16 */
17function decodeBuffer(buffer) {
18 try {
19 return iconv.decode(buffer, 'utf-8');
20 } catch (error) {
21 console.log('Failed to decode buffer:', error);
22 return buffer.toString('utf-8');
23 }
24}
25
26const router = express.Router();
2716
28router.post('/libre', jsonParser, async (request, response) => {17router.post('/libre', jsonParser, async (request, response) => {
29 const key = readSecret(request.user.directories, SECRET_KEYS.LIBRE);18 const key = readSecret(request.user.directories, SECRET_KEYS.LIBRE);
@@ -70,6 +59,7 @@ router.post('/libre', jsonParser, async (request, response) => {
70 return response.sendStatus(result.status);59 return response.sendStatus(result.status);
71 }60 }
7261
62 /** @type {any} */
73 const json = await result.json();63 const json = await result.json();
74 console.log('Translated text: ' + json.translatedText);64 console.log('Translated text: ' + json.translatedText);
7565
@@ -82,7 +72,6 @@ router.post('/libre', jsonParser, async (request, response) => {
8272
83router.post('/google', jsonParser, async (request, response) => {73router.post('/google', jsonParser, async (request, response) => {
84 try {74 try {
85 const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser');
86 const text = request.body.text;75 const text = request.body.text;
87 const lang = request.body.lang;76 const lang = request.body.lang;
8877
@@ -92,31 +81,12 @@ router.post('/google', jsonParser, async (request, response) => {
9281
93 console.log('Input text: ' + text);82 console.log('Input text: ' + text);
9483
95 const url = generateRequestUrl(text, { to: lang });84 const result = await googleTranslateApi(text, { to: lang, forceBatch: false });
85 const translatedText = Array.isArray(result) ? result.map(x => x.text).join('') : result.text;
9686
97 https.get(url, (resp) => {87 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
98 const data = [];88 console.log('Translated text: ' + translatedText);
9989 return response.send(translatedText);
100 resp.on('data', (chunk) => {
101 data.push(chunk);
102 });
103
104 resp.on('end', () => {
105 try {
106 const decodedData = decodeBuffer(Buffer.concat(data));
107 const result = normaliseResponse(JSON.parse(decodedData));
108 console.log('Translated text: ' + result.text);
109 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
110 return response.send(result.text);
111 } catch (error) {
112 console.log('Translation error', error);
113 return response.sendStatus(500);
114 }
115 });
116 }).on('error', (err) => {
117 console.log('Translation error: ' + err.message);
118 return response.sendStatus(500);
119 });
120 } catch (error) {90 } catch (error) {
121 console.log('Translation error', error);91 console.log('Translation error', error);
122 return response.sendStatus(500);92 return response.sendStatus(500);
@@ -151,7 +121,6 @@ router.post('/yandex', jsonParser, async (request, response) => {
151 headers: {121 headers: {
152 'Content-Type': 'application/x-www-form-urlencoded',122 'Content-Type': 'application/x-www-form-urlencoded',
153 },123 },
154 timeout: 0,
155 });124 });
156125
157 if (!result.ok) {126 if (!result.ok) {
@@ -160,6 +129,7 @@ router.post('/yandex', jsonParser, async (request, response) => {
160 return response.sendStatus(500);129 return response.sendStatus(500);
161 }130 }
162131
132 /** @type {any} */
163 const json = await result.json();133 const json = await result.json();
164 const translated = json.text.join();134 const translated = json.text.join();
165 console.log('Translated text: ' + translated);135 console.log('Translated text: ' + translated);
@@ -257,7 +227,6 @@ router.post('/deepl', jsonParser, async (request, response) => {
257 'Authorization': `DeepL-Auth-Key ${key}`,227 'Authorization': `DeepL-Auth-Key ${key}`,
258 'Content-Type': 'application/x-www-form-urlencoded',228 'Content-Type': 'application/x-www-form-urlencoded',
259 },229 },
260 timeout: 0,
261 });230 });
262231
263 if (!result.ok) {232 if (!result.ok) {
@@ -266,6 +235,7 @@ router.post('/deepl', jsonParser, async (request, response) => {
266 return response.sendStatus(result.status);235 return response.sendStatus(result.status);
267 }236 }
268237
238 /** @type {any} */
269 const json = await result.json();239 const json = await result.json();
270 console.log('Translated text: ' + json.translations[0].text);240 console.log('Translated text: ' + json.translations[0].text);
271241
@@ -310,7 +280,6 @@ router.post('/onering', jsonParser, async (request, response) => {
310280
311 const result = await fetch(fetchUrl, {281 const result = await fetch(fetchUrl, {
312 method: 'GET',282 method: 'GET',
313 timeout: 0,
314 });283 });
315284
316 if (!result.ok) {285 if (!result.ok) {
@@ -319,6 +288,7 @@ router.post('/onering', jsonParser, async (request, response) => {
319 return response.sendStatus(result.status);288 return response.sendStatus(result.status);
320 }289 }
321290
291 /** @type {any} */
322 const data = await result.json();292 const data = await result.json();
323 console.log('Translated text: ' + data.result);293 console.log('Translated text: ' + data.result);
324294
@@ -366,7 +336,6 @@ router.post('/deeplx', jsonParser, async (request, response) => {
366 'Accept': 'application/json',336 'Accept': 'application/json',
367 'Content-Type': 'application/json',337 'Content-Type': 'application/json',
368 },338 },
369 timeout: 0,
370 });339 });
371340
372 if (!result.ok) {341 if (!result.ok) {
@@ -375,6 +344,7 @@ router.post('/deeplx', jsonParser, async (request, response) => {
375 return response.sendStatus(result.status);344 return response.sendStatus(result.status);
376 }345 }
377346
347 /** @type {any} */
378 const json = await result.json();348 const json = await result.json();
379 console.log('Translated text: ' + json.data);349 console.log('Translated text: ' + json.data);
380350
@@ -386,7 +356,6 @@ router.post('/deeplx', jsonParser, async (request, response) => {
386});356});
387357
388router.post('/bing', jsonParser, async (request, response) => {358router.post('/bing', jsonParser, async (request, response) => {
389 const bingTranslateApi = require('bing-translate-api');
390 const text = request.body.text;359 const text = request.body.text;
391 let lang = request.body.lang;360 let lang = request.body.lang;
392361
@@ -408,5 +377,3 @@ router.post('/bing', jsonParser, async (request, response) => {
408 return response.sendStatus(500);377 return response.sendStatus(500);
409 });378 });
410});379});
411
412module.exports = { router };
src/endpoints/users-admin.js+17 -20
@@ -1,10 +1,11 @@
1const fsPromises = require('fs').promises;1import { promises as fsPromises } from 'node:fs';
2const storage = require('node-persist');2
3const express = require('express');3import storage from 'node-persist';
4const lodash = require('lodash');4import express from 'express';
5const { jsonParser } = require('../express-common');5import lodash from 'lodash';
6const { checkForNewContent } = require('./content-manager');6import { jsonParser } from '../express-common.js';
7const {7import { checkForNewContent } from './content-manager.js';
8import {
8 KEY_PREFIX,9 KEY_PREFIX,
9 toKey,10 toKey,
10 requireAdminMiddleware,11 requireAdminMiddleware,
@@ -14,17 +15,17 @@ const {
14 getPasswordHash,15 getPasswordHash,
15 getUserDirectories,16 getUserDirectories,
16 ensurePublicDirectoriesExist,17 ensurePublicDirectoriesExist,
17} = require('../users');18} from '../users.js';
18const { DEFAULT_USER } = require('../constants');19import { DEFAULT_USER } from '../constants.js';
1920
20const router = express.Router();21export const router = express.Router();
2122
22router.post('/get', requireAdminMiddleware, jsonParser, async (_request, response) => {23router.post('/get', requireAdminMiddleware, jsonParser, async (_request, response) => {
23 try {24 try {
24 /** @type {import('../users').User[]} */25 /** @type {import('../users.js').User[]} */
25 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));26 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
2627
27 /** @type {Promise<import('../users').UserViewModel>[]} */28 /** @type {Promise<import('../users.js').UserViewModel>[]} */
28 const viewModelPromises = users29 const viewModelPromises = users
29 .map(user => new Promise(resolve => {30 .map(user => new Promise(resolve => {
30 getUserAvatar(user.handle).then(avatar =>31 getUserAvatar(user.handle).then(avatar =>
@@ -61,7 +62,7 @@ router.post('/disable', requireAdminMiddleware, jsonParser, async (request, resp
61 return response.status(400).json({ error: 'Cannot disable yourself' });62 return response.status(400).json({ error: 'Cannot disable yourself' });
62 }63 }
6364
64 /** @type {import('../users').User} */65 /** @type {import('../users.js').User} */
65 const user = await storage.getItem(toKey(request.body.handle));66 const user = await storage.getItem(toKey(request.body.handle));
6667
67 if (!user) {68 if (!user) {
@@ -85,7 +86,7 @@ router.post('/enable', requireAdminMiddleware, jsonParser, async (request, respo
85 return response.status(400).json({ error: 'Missing required fields' });86 return response.status(400).json({ error: 'Missing required fields' });
86 }87 }
8788
88 /** @type {import('../users').User} */89 /** @type {import('../users.js').User} */
89 const user = await storage.getItem(toKey(request.body.handle));90 const user = await storage.getItem(toKey(request.body.handle));
9091
91 if (!user) {92 if (!user) {
@@ -109,7 +110,7 @@ router.post('/promote', requireAdminMiddleware, jsonParser, async (request, resp
109 return response.status(400).json({ error: 'Missing required fields' });110 return response.status(400).json({ error: 'Missing required fields' });
110 }111 }
111112
112 /** @type {import('../users').User} */113 /** @type {import('../users.js').User} */
113 const user = await storage.getItem(toKey(request.body.handle));114 const user = await storage.getItem(toKey(request.body.handle));
114115
115 if (!user) {116 if (!user) {
@@ -138,7 +139,7 @@ router.post('/demote', requireAdminMiddleware, jsonParser, async (request, respo
138 return response.status(400).json({ error: 'Cannot demote yourself' });139 return response.status(400).json({ error: 'Cannot demote yourself' });
139 }140 }
140141
141 /** @type {import('../users').User} */142 /** @type {import('../users.js').User} */
142 const user = await storage.getItem(toKey(request.body.handle));143 const user = await storage.getItem(toKey(request.body.handle));
143144
144 if (!user) {145 if (!user) {
@@ -249,7 +250,3 @@ router.post('/slugify', requireAdminMiddleware, jsonParser, async (request, resp
249 return response.sendStatus(500);250 return response.sendStatus(500);
250 }251 }
251});252});
252
253module.exports = {
254 router,
255};
src/endpoints/users-private.js+17 -20
@@ -1,18 +1,19 @@
1const path = require('path');1import path from 'node:path';
2const fsPromises = require('fs').promises;2import { promises as fsPromises } from 'node:fs';
3const storage = require('node-persist');3import crypto from 'node:crypto';
4const express = require('express');4
5const crypto = require('crypto');5import storage from 'node-persist';
6const { jsonParser } = require('../express-common');6import express from 'express';
7const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } = require('../users');7
8const { SETTINGS_FILE } = require('../constants');8import { jsonParser } from '../express-common.js';
9const contentManager = require('./content-manager');9import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js';
10const { color, Cache } = require('../util');10import { SETTINGS_FILE } from '../constants.js';
11const { checkForNewContent } = require('./content-manager');11import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
12import { color, Cache } from '../util.js';
1213
13const RESET_CACHE = new Cache(5 * 60 * 1000);14const RESET_CACHE = new Cache(5 * 60 * 1000);
1415
15const router = express.Router();16export const router = express.Router();
1617
17router.post('/logout', async (request, response) => {18router.post('/logout', async (request, response) => {
18 try {19 try {
@@ -70,7 +71,7 @@ router.post('/change-avatar', jsonParser, async (request, response) => {
70 return response.status(400).json({ error: 'Invalid data URL' });71 return response.status(400).json({ error: 'Invalid data URL' });
71 }72 }
7273
73 /** @type {import('../users').User} */74 /** @type {import('../users.js').User} */
74 const user = await storage.getItem(toKey(request.body.handle));75 const user = await storage.getItem(toKey(request.body.handle));
7576
76 if (!user) {77 if (!user) {
@@ -99,7 +100,7 @@ router.post('/change-password', jsonParser, async (request, response) => {
99 return response.status(403).json({ error: 'Unauthorized' });100 return response.status(403).json({ error: 'Unauthorized' });
100 }101 }
101102
102 /** @type {import('../users').User} */103 /** @type {import('../users.js').User} */
103 const user = await storage.getItem(toKey(request.body.handle));104 const user = await storage.getItem(toKey(request.body.handle));
104105
105 if (!user) {106 if (!user) {
@@ -166,7 +167,7 @@ router.post('/reset-settings', jsonParser, async (request, response) => {
166167
167 const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE);168 const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE);
168 await fsPromises.rm(pathToFile, { force: true });169 await fsPromises.rm(pathToFile, { force: true });
169 await contentManager.checkForNewContent([request.user.directories], [contentManager.CONTENT_TYPES.SETTINGS]);170 await checkForNewContent([request.user.directories], [CONTENT_TYPES.SETTINGS]);
170171
171 return response.sendStatus(204);172 return response.sendStatus(204);
172 } catch (error) {173 } catch (error) {
@@ -187,7 +188,7 @@ router.post('/change-name', jsonParser, async (request, response) => {
187 return response.status(403).json({ error: 'Unauthorized' });188 return response.status(403).json({ error: 'Unauthorized' });
188 }189 }
189190
190 /** @type {import('../users').User} */191 /** @type {import('../users.js').User} */
191 const user = await storage.getItem(toKey(request.body.handle));192 const user = await storage.getItem(toKey(request.body.handle));
192193
193 if (!user) {194 if (!user) {
@@ -251,7 +252,3 @@ router.post('/reset-step2', jsonParser, async (request, response) => {
251 return response.sendStatus(500);252 return response.sendStatus(500);
252 }253 }
253});254});
254
255module.exports = {
256 router,
257};
src/endpoints/users-public.js+14 -17
@@ -1,15 +1,16 @@
1const crypto = require('crypto');1import crypto from 'node:crypto';
2const storage = require('node-persist');2
3const express = require('express');3import storage from 'node-persist';
4const { RateLimiterMemory, RateLimiterRes } = require('rate-limiter-flexible');4import express from 'express';
5const { jsonParser, getIpFromRequest } = require('../express-common');5import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
6const { color, Cache, getConfigValue } = require('../util');6import { jsonParser, getIpFromRequest } from '../express-common.js';
7const { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } = require('../users');7import { color, Cache, getConfigValue } from '../util.js';
8import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
89
9const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);10const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
10const MFA_CACHE = new Cache(5 * 60 * 1000);11const MFA_CACHE = new Cache(5 * 60 * 1000);
1112
12const router = express.Router();13export const router = express.Router();
13const loginLimiter = new RateLimiterMemory({14const loginLimiter = new RateLimiterMemory({
14 points: 5,15 points: 5,
15 duration: 60,16 duration: 60,
@@ -25,10 +26,10 @@ router.post('/list', async (_request, response) => {
25 return response.sendStatus(204);26 return response.sendStatus(204);
26 }27 }
2728
28 /** @type {import('../users').User[]} */29 /** @type {import('../users.js').User[]} */
29 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));30 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
3031
31 /** @type {Promise<import('../users').UserViewModel>[]} */32 /** @type {Promise<import('../users.js').UserViewModel>[]} */
32 const viewModelPromises = users33 const viewModelPromises = users
33 .filter(x => x.enabled)34 .filter(x => x.enabled)
34 .map(user => new Promise(async (resolve) => {35 .map(user => new Promise(async (resolve) => {
@@ -62,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
62 const ip = getIpFromRequest(request);63 const ip = getIpFromRequest(request);
63 await loginLimiter.consume(ip);64 await loginLimiter.consume(ip);
6465
65 /** @type {import('../users').User} */66 /** @type {import('../users.js').User} */
66 const user = await storage.getItem(toKey(request.body.handle));67 const user = await storage.getItem(toKey(request.body.handle));
6768
68 if (!user) {69 if (!user) {
@@ -110,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
110 const ip = getIpFromRequest(request);111 const ip = getIpFromRequest(request);
111 await recoverLimiter.consume(ip);112 await recoverLimiter.consume(ip);
112113
113 /** @type {import('../users').User} */114 /** @type {import('../users.js').User} */
114 const user = await storage.getItem(toKey(request.body.handle));115 const user = await storage.getItem(toKey(request.body.handle));
115116
116 if (!user) {117 if (!user) {
@@ -147,7 +148,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
147 return response.status(400).json({ error: 'Missing required fields' });148 return response.status(400).json({ error: 'Missing required fields' });
148 }149 }
149150
150 /** @type {import('../users').User} */151 /** @type {import('../users.js').User} */
151 const user = await storage.getItem(toKey(request.body.handle));152 const user = await storage.getItem(toKey(request.body.handle));
152 const ip = getIpFromRequest(request);153 const ip = getIpFromRequest(request);
153154
@@ -193,7 +194,3 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
193 return response.sendStatus(500);194 return response.sendStatus(500);
194 }195 }
195});196});
196
197module.exports = {
198 router,
199};
src/endpoints/vectors.js+46 -36
@@ -1,10 +1,22 @@
1const vectra = require('vectra');1import path from 'node:path';
2const path = require('path');2import fs from 'node:fs';
3const fs = require('fs');3
4const express = require('express');4import vectra from 'vectra';
5const sanitize = require('sanitize-filename');5import express from 'express';
6const { jsonParser } = require('../express-common');6import sanitize from 'sanitize-filename';
7const { getConfigValue } = require('../util');7
8import { jsonParser } from '../express-common.js';
9import { getConfigValue } from '../util.js';
10
11import { getNomicAIBatchVector, getNomicAIVector } from '../vectors/nomicai-vectors.js';
12import { getOpenAIVector, getOpenAIBatchVector } from '../vectors/openai-vectors.js';
13import { getTransformersVector, getTransformersBatchVector } from '../vectors/embedding.js';
14import { getExtrasVector, getExtrasBatchVector } from '../vectors/extras-vectors.js';
15import { getMakerSuiteVector, getMakerSuiteBatchVector } from '../vectors/makersuite-vectors.js';
16import { getCohereVector, getCohereBatchVector } from '../vectors/cohere-vectors.js';
17import { getLlamaCppVector, getLlamaCppBatchVector } from '../vectors/llamacpp-vectors.js';
18import { getVllmVector, getVllmBatchVector } from '../vectors/vllm-vectors.js';
19import { getOllamaVector, getOllamaBatchVector } from '../vectors/ollama-vectors.js';
820
9// Don't forget to add new sources to the SOURCES array21// Don't forget to add new sources to the SOURCES array
10const SOURCES = [22const SOURCES = [
@@ -27,31 +39,31 @@ const SOURCES = [
27 * @param {Object} sourceSettings - Settings for the source, if it needs any39 * @param {Object} sourceSettings - Settings for the source, if it needs any
28 * @param {string} text - The text to get the vector for40 * @param {string} text - The text to get the vector for
29 * @param {boolean} isQuery - If the text is a query for embedding search41 * @param {boolean} isQuery - If the text is a query for embedding search
30 * @param {import('../users').UserDirectoryList} directories - The directories object for the user42 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
31 * @returns {Promise<number[]>} - The vector for the text43 * @returns {Promise<number[]>} - The vector for the text
32 */44 */
33async function getVector(source, sourceSettings, text, isQuery, directories) {45async function getVector(source, sourceSettings, text, isQuery, directories) {
34 switch (source) {46 switch (source) {
35 case 'nomicai':47 case 'nomicai':
36 return require('../vectors/nomicai-vectors').getNomicAIVector(text, source, directories);48 return getNomicAIVector(text, source, directories);
37 case 'togetherai':49 case 'togetherai':
38 case 'mistral':50 case 'mistral':
39 case 'openai':51 case 'openai':
40 return require('../vectors/openai-vectors').getOpenAIVector(text, source, directories, sourceSettings.model);52 return getOpenAIVector(text, source, directories, sourceSettings.model);
41 case 'transformers':53 case 'transformers':
42 return require('../vectors/embedding').getTransformersVector(text);54 return getTransformersVector(text);
43 case 'extras':55 case 'extras':
44 return require('../vectors/extras-vectors').getExtrasVector(text, sourceSettings.extrasUrl, sourceSettings.extrasKey);56 return getExtrasVector(text, sourceSettings.extrasUrl, sourceSettings.extrasKey);
45 case 'palm':57 case 'palm':
46 return require('../vectors/makersuite-vectors').getMakerSuiteVector(text, directories);58 return getMakerSuiteVector(text, directories);
47 case 'cohere':59 case 'cohere':
48 return require('../vectors/cohere-vectors').getCohereVector(text, isQuery, directories, sourceSettings.model);60 return getCohereVector(text, isQuery, directories, sourceSettings.model);
49 case 'llamacpp':61 case 'llamacpp':
50 return require('../vectors/llamacpp-vectors').getLlamaCppVector(text, sourceSettings.apiUrl, directories);62 return getLlamaCppVector(text, sourceSettings.apiUrl, directories);
51 case 'vllm':63 case 'vllm':
52 return require('../vectors/vllm-vectors').getVllmVector(text, sourceSettings.apiUrl, sourceSettings.model, directories);64 return getVllmVector(text, sourceSettings.apiUrl, sourceSettings.model, directories);
53 case 'ollama':65 case 'ollama':
54 return require('../vectors/ollama-vectors').getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);66 return getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);
55 }67 }
5668
57 throw new Error(`Unknown vector source ${source}`);69 throw new Error(`Unknown vector source ${source}`);
@@ -63,7 +75,7 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
63 * @param {Object} sourceSettings - Settings for the source, if it needs any75 * @param {Object} sourceSettings - Settings for the source, if it needs any
64 * @param {string[]} texts - The array of texts to get the vector for76 * @param {string[]} texts - The array of texts to get the vector for
65 * @param {boolean} isQuery - If the text is a query for embedding search77 * @param {boolean} isQuery - If the text is a query for embedding search
66 * @param {import('../users').UserDirectoryList} directories - The directories object for the user78 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
67 * @returns {Promise<number[][]>} - The array of vectors for the texts79 * @returns {Promise<number[][]>} - The array of vectors for the texts
68 */80 */
69async function getBatchVector(source, sourceSettings, texts, isQuery, directories) {81async function getBatchVector(source, sourceSettings, texts, isQuery, directories) {
@@ -74,33 +86,33 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
74 for (let batch of batches) {86 for (let batch of batches) {
75 switch (source) {87 switch (source) {
76 case 'nomicai':88 case 'nomicai':
77 results.push(...await require('../vectors/nomicai-vectors').getNomicAIBatchVector(batch, source, directories));89 results.push(...await getNomicAIBatchVector(batch, source, directories));
78 break;90 break;
79 case 'togetherai':91 case 'togetherai':
80 case 'mistral':92 case 'mistral':
81 case 'openai':93 case 'openai':
82 results.push(...await require('../vectors/openai-vectors').getOpenAIBatchVector(batch, source, directories, sourceSettings.model));94 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
83 break;95 break;
84 case 'transformers':96 case 'transformers':
85 results.push(...await require('../vectors/embedding').getTransformersBatchVector(batch));97 results.push(...await getTransformersBatchVector(batch));
86 break;98 break;
87 case 'extras':99 case 'extras':
88 results.push(...await require('../vectors/extras-vectors').getExtrasBatchVector(batch, sourceSettings.extrasUrl, sourceSettings.extrasKey));100 results.push(...await getExtrasBatchVector(batch, sourceSettings.extrasUrl, sourceSettings.extrasKey));
89 break;101 break;
90 case 'palm':102 case 'palm':
91 results.push(...await require('../vectors/makersuite-vectors').getMakerSuiteBatchVector(batch, directories));103 results.push(...await getMakerSuiteBatchVector(batch, directories));
92 break;104 break;
93 case 'cohere':105 case 'cohere':
94 results.push(...await require('../vectors/cohere-vectors').getCohereBatchVector(batch, isQuery, directories, sourceSettings.model));106 results.push(...await getCohereBatchVector(batch, isQuery, directories, sourceSettings.model));
95 break;107 break;
96 case 'llamacpp':108 case 'llamacpp':
97 results.push(...await require('../vectors/llamacpp-vectors').getLlamaCppBatchVector(batch, sourceSettings.apiUrl, directories));109 results.push(...await getLlamaCppBatchVector(batch, sourceSettings.apiUrl, directories));
98 break;110 break;
99 case 'vllm':111 case 'vllm':
100 results.push(...await require('../vectors/vllm-vectors').getVllmBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, directories));112 results.push(...await getVllmBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, directories));
101 break;113 break;
102 case 'ollama':114 case 'ollama':
103 results.push(...await require('../vectors/ollama-vectors').getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories));115 results.push(...await getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories));
104 break;116 break;
105 default:117 default:
106 throw new Error(`Unknown vector source ${source}`);118 throw new Error(`Unknown vector source ${source}`);
@@ -183,7 +195,7 @@ function getModelScope(sourceSettings) {
183195
184/**196/**
185 * Gets the index for the vector collection197 * Gets the index for the vector collection
186 * @param {import('../users').UserDirectoryList} directories - User directories198 * @param {import('../users.js').UserDirectoryList} directories - User directories
187 * @param {string} collectionId - The collection ID199 * @param {string} collectionId - The collection ID
188 * @param {string} source - The source of the vector200 * @param {string} source - The source of the vector
189 * @param {object} sourceSettings - The model for the source201 * @param {object} sourceSettings - The model for the source
@@ -203,7 +215,7 @@ async function getIndex(directories, collectionId, source, sourceSettings) {
203215
204/**216/**
205 * Inserts items into the vector collection217 * Inserts items into the vector collection
206 * @param {import('../users').UserDirectoryList} directories - User directories218 * @param {import('../users.js').UserDirectoryList} directories - User directories
207 * @param {string} collectionId - The collection ID219 * @param {string} collectionId - The collection ID
208 * @param {string} source - The source of the vector220 * @param {string} source - The source of the vector
209 * @param {Object} sourceSettings - Settings for the source, if it needs any221 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -227,7 +239,7 @@ async function insertVectorItems(directories, collectionId, source, sourceSettin
227239
228/**240/**
229 * Gets the hashes of the items in the vector collection241 * Gets the hashes of the items in the vector collection
230 * @param {import('../users').UserDirectoryList} directories - User directories242 * @param {import('../users.js').UserDirectoryList} directories - User directories
231 * @param {string} collectionId - The collection ID243 * @param {string} collectionId - The collection ID
232 * @param {string} source - The source of the vector244 * @param {string} source - The source of the vector
233 * @param {Object} sourceSettings - Settings for the source, if it needs any245 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -244,7 +256,7 @@ async function getSavedHashes(directories, collectionId, source, sourceSettings)
244256
245/**257/**
246 * Deletes items from the vector collection by hash258 * Deletes items from the vector collection by hash
247 * @param {import('../users').UserDirectoryList} directories - User directories259 * @param {import('../users.js').UserDirectoryList} directories - User directories
248 * @param {string} collectionId - The collection ID260 * @param {string} collectionId - The collection ID
249 * @param {string} source - The source of the vector261 * @param {string} source - The source of the vector
250 * @param {Object} sourceSettings - Settings for the source, if it needs any262 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -265,7 +277,7 @@ async function deleteVectorItems(directories, collectionId, source, sourceSettin
265277
266/**278/**
267 * Gets the hashes of the items in the vector collection that match the search text279 * Gets the hashes of the items in the vector collection that match the search text
268 * @param {import('../users').UserDirectoryList} directories - User directories280 * @param {import('../users.js').UserDirectoryList} directories - User directories
269 * @param {string} collectionId - The collection ID281 * @param {string} collectionId - The collection ID
270 * @param {string} source - The source of the vector282 * @param {string} source - The source of the vector
271 * @param {Object} sourceSettings - Settings for the source, if it needs any283 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -286,7 +298,7 @@ async function queryCollection(directories, collectionId, source, sourceSettings
286298
287/**299/**
288 * Queries multiple collections for the given search queries. Returns the overall top K results.300 * Queries multiple collections for the given search queries. Returns the overall top K results.
289 * @param {import('../users').UserDirectoryList} directories - User directories301 * @param {import('../users.js').UserDirectoryList} directories - User directories
290 * @param {string[]} collectionIds - The collection IDs to query302 * @param {string[]} collectionIds - The collection IDs to query
291 * @param {string} source - The source of the vector303 * @param {string} source - The source of the vector
292 * @param {Object} sourceSettings - Settings for the source, if it needs any304 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -359,7 +371,7 @@ async function regenerateCorruptedIndexErrorHandler(req, res, error) {
359 return res.sendStatus(500);371 return res.sendStatus(500);
360}372}
361373
362const router = express.Router();374export const router = express.Router();
363375
364router.post('/query', jsonParser, async (req, res) => {376router.post('/query', jsonParser, async (req, res) => {
365 try {377 try {
@@ -495,5 +507,3 @@ router.post('/purge', jsonParser, async (req, res) => {
495 return res.sendStatus(500);507 return res.sendStatus(500);
496 }508 }
497});509});
498
499module.exports = { router };
src/endpoints/worldinfo.js+10 -11
@@ -1,19 +1,20 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const express = require('express');
4const sanitize = require('sanitize-filename');
5const writeFileAtomicSync = require('write-file-atomic').sync;
63
7const { jsonParser, urlencodedParser } = require('../express-common');4import express from 'express';
5import sanitize from 'sanitize-filename';
6import { sync as writeFileAtomicSync } from 'write-file-atomic';
7
8import { jsonParser, urlencodedParser } from '../express-common.js';
89
9/**10/**
10 * Reads a World Info file and returns its contents11 * Reads a World Info file and returns its contents
11 * @param {import('../users').UserDirectoryList} directories User directories12 * @param {import('../users.js').UserDirectoryList} directories User directories
12 * @param {string} worldInfoName Name of the World Info file13 * @param {string} worldInfoName Name of the World Info file
13 * @param {boolean} allowDummy If true, returns an empty object if the file doesn't exist14 * @param {boolean} allowDummy If true, returns an empty object if the file doesn't exist
14 * @returns {object} World Info file contents15 * @returns {object} World Info file contents
15 */16 */
16function readWorldInfoFile(directories, worldInfoName, allowDummy) {17export function readWorldInfoFile(directories, worldInfoName, allowDummy) {
17 const dummyObject = allowDummy ? { entries: {} } : null;18 const dummyObject = allowDummy ? { entries: {} } : null;
1819
19 if (!worldInfoName) {20 if (!worldInfoName) {
@@ -33,7 +34,7 @@ function readWorldInfoFile(directories, worldInfoName, allowDummy) {
33 return worldInfo;34 return worldInfo;
34}35}
3536
36const router = express.Router();37export const router = express.Router();
3738
38router.post('/get', jsonParser, (request, response) => {39router.post('/get', jsonParser, (request, response) => {
39 if (!request.body?.name) {40 if (!request.body?.name) {
@@ -122,5 +123,3 @@ router.post('/edit', jsonParser, (request, response) => {
122123
123 return response.send({ ok: true });124 return response.send({ ok: true });
124});125});
125
126module.exports = { router, readWorldInfoFile };
src/express-common.js+11 -12
@@ -1,28 +1,27 @@
1const express = require('express');1import express from 'express';
2const ipaddr = require('ipaddr.js');2import ipaddr from 'ipaddr.js';
33
4// Instantiate parser middleware here with application-level size limits4// Instantiate parser middleware here with application-level size limits
5const jsonParser = express.json({ limit: '200mb' });5export const jsonParser = express.json({ limit: '200mb' });
6const urlencodedParser = express.urlencoded({ extended: true, limit: '200mb' });6export const urlencodedParser = express.urlencoded({ extended: true, limit: '200mb' });
77
8/**8/**
9 * Gets the IP address of the client from the request object.9 * Gets the IP address of the client from the request object.
10 * @param {import('express'.Request)} req Request object10 * @param {import('express').Request} req Request object
11 * @returns {string} IP address of the client11 * @returns {string} IP address of the client
12 */12 */
13function getIpFromRequest(req) {13export function getIpFromRequest(req) {
14 let clientIp = req.connection.remoteAddress;14 let clientIp = req.socket.remoteAddress;
15 if (!clientIp) {
16 return 'unknown';
17 }
15 let ip = ipaddr.parse(clientIp);18 let ip = ipaddr.parse(clientIp);
16 // Check if the IP address is IPv4-mapped IPv6 address19 // Check if the IP address is IPv4-mapped IPv6 address
17 if (ip.kind() === 'ipv6' && ip instanceof ipaddr.IPv6 && ip.isIPv4MappedAddress()) {20 if (ip.kind() === 'ipv6' && ip instanceof ipaddr.IPv6 && ip.isIPv4MappedAddress()) {
18 const ipv4 = ip.toIPv4Address().toString();21 const ipv4 = ip.toIPv4Address().toString();
19 clientIp = ipv4;22 clientIp = ipv4;
20 } else {23 } else {
21 clientIp = ip;24 clientIp = ip.toString();
22 clientIp = clientIp.toString();
23 }25 }
24 return clientIp;26 return clientIp;
25}27}
26
27
28module.exports = { jsonParser, urlencodedParser, getIpFromRequest };
src/middleware/basicAuth.js+5 -4
@@ -2,9 +2,10 @@
2 * When applied, this middleware will ensure the request contains the required header for basic authentication and only2 * When applied, this middleware will ensure the request contains the required header for basic authentication and only
3 * allow access to the endpoint after successful authentication.3 * allow access to the endpoint after successful authentication.
4 */4 */
5const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js');5import { Buffer } from 'node:buffer';
6const { getConfig, getConfigValue } = require('../util.js');6import storage from 'node-persist';
7const storage = require('node-persist');7import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
8import { getConfig, getConfigValue } from '../util.js';
89
9const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);10const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
10const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);11const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
@@ -49,4 +50,4 @@ const basicAuthMiddleware = async function (request, response, callback) {
49 return unauthorizedResponse(response);50 return unauthorizedResponse(response);
50};51};
5152
52module.exports = basicAuthMiddleware;53export default basicAuthMiddleware;
src/middleware/multerMonkeyPatch.js+3 -3
@@ -1,3 +1,5 @@
1import { Buffer } from 'node:buffer';
2
1/**3/**
2 * Decodes a file name from Latin1 to UTF-8.4 * Decodes a file name from Latin1 to UTF-8.
3 * @param {string} str Input string5 * @param {string} str Input string
@@ -14,7 +16,7 @@ function decodeFileName(str) {
14 * @param {import('express').Response} _res Response16 * @param {import('express').Response} _res Response
15 * @param {import('express').NextFunction} next Next middleware17 * @param {import('express').NextFunction} next Next middleware
16 */18 */
17function multerMonkeyPatch(req, _res, next) {19export default function multerMonkeyPatch(req, _res, next) {
18 try {20 try {
19 if (req.file) {21 if (req.file) {
20 req.file.originalname = decodeFileName(req.file.originalname);22 req.file.originalname = decodeFileName(req.file.originalname);
@@ -26,5 +28,3 @@ function multerMonkeyPatch(req, _res, next) {
26 next();28 next();
27 }29 }
28}30}
29
30module.exports = multerMonkeyPatch;
src/middleware/whitelist.js+7 -8
@@ -1,9 +1,10 @@
1const path = require('path');1import path from 'node:path';
2const fs = require('fs');2import fs from 'node:fs';
3const ipMatching = require('ip-matching');3import process from 'node:process';
4import ipMatching from 'ip-matching';
45
5const { getIpFromRequest } = require('../express-common');6import { getIpFromRequest } from '../express-common.js';
6const { color, getConfigValue } = require('../util');7import { color, getConfigValue } from '../util.js';
78
8const whitelistPath = path.join(process.cwd(), './whitelist.txt');9const whitelistPath = path.join(process.cwd(), './whitelist.txt');
9const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);10const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
@@ -50,7 +51,7 @@ function getForwardedIp(req) {
50 * @param {boolean} listen If listen mode is enabled via config or command line51 * @param {boolean} listen If listen mode is enabled via config or command line
51 * @returns {import('express').RequestHandler} The middleware function52 * @returns {import('express').RequestHandler} The middleware function
52 */53 */
53function whitelistMiddleware(whitelistMode, listen) {54export default function whitelistMiddleware(whitelistMode, listen) {
54 return function (req, res, next) {55 return function (req, res, next) {
55 const clientIp = getIpFromRequest(req);56 const clientIp = getIpFromRequest(req);
56 const forwardedIp = getForwardedIp(req);57 const forwardedIp = getForwardedIp(req);
@@ -82,5 +83,3 @@ function whitelistMiddleware(whitelistMode, listen) {
82 next();83 next();
83 };84 };
84}85}
85
86module.exports = whitelistMiddleware;
src/plugin-loader.js+17 -24
@@ -1,8 +1,9 @@
1const fs = require('fs');1import fs from 'node:fs';
2const path = require('path');2import path from 'node:path';
3const url = require('url');3import url from 'node:url';
4const express = require('express');4
5const { getConfigValue } = require('./util');5import express from 'express';
6import { getConfigValue } from './util.js';
6const enableServerPlugins = getConfigValue('enableServerPlugins', false);7const enableServerPlugins = getConfigValue('enableServerPlugins', false);
78
8/**9/**
@@ -16,7 +17,7 @@ const loadedPlugins = new Map();
16 * @param {string} file Path to file17 * @param {string} file Path to file
17 * @returns {boolean} True if file is a CommonJS module18 * @returns {boolean} True if file is a CommonJS module
18 */19 */
19const isCommonJS = (file) => path.extname(file) === '.js';20const isCommonJS = (file) => path.extname(file) === '.js' || path.extname(file) === '.cjs';
2021
21/**22/**
22 * Determine if a file is an ECMAScript module.23 * Determine if a file is an ECMAScript module.
@@ -32,9 +33,9 @@ const isESModule = (file) => path.extname(file) === '.mjs';
32 * @returns {Promise<Function>} Promise that resolves when all plugins are loaded. Resolves to a "cleanup" function to33 * @returns {Promise<Function>} Promise that resolves when all plugins are loaded. Resolves to a "cleanup" function to
33 * be called before the server shuts down.34 * be called before the server shuts down.
34 */35 */
35async function loadPlugins(app, pluginsPath) {36export async function loadPlugins(app, pluginsPath) {
36 const exitHooks = [];37 const exitHooks = [];
37 const emptyFn = () => {};38 const emptyFn = () => { };
3839
39 // Server plugins are disabled.40 // Server plugins are disabled.
40 if (!enableServerPlugins) {41 if (!enableServerPlugins) {
@@ -89,19 +90,15 @@ async function loadFromDirectory(app, pluginDirectoryPath, exitHooks) {
89 }90 }
90 }91 }
9192
92 // Plugin is a CommonJS module.93 // Plugin is a module file.
93 const cjsFilePath = path.join(pluginDirectoryPath, 'index.js');94 const fileTypes = ['index.js', 'index.cjs', 'index.mjs'];
94 if (fs.existsSync(cjsFilePath)) {
95 if (await loadFromFile(app, cjsFilePath, exitHooks)) {
96 return;
97 }
98 }
9995
100 // Plugin is an ECMAScript module.96 for (const fileType of fileTypes) {
101 const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs');97 const filePath = path.join(pluginDirectoryPath, fileType);
102 if (fs.existsSync(esmFilePath)) {98 if (fs.existsSync(filePath)) {
103 if (await loadFromFile(app, esmFilePath, exitHooks)) {99 if (await loadFromFile(app, filePath, exitHooks)) {
104 return;100 return;
101 }
105 }102 }
106 }103 }
107}104}
@@ -217,7 +214,3 @@ async function initPlugin(app, plugin, exitHooks) {
217214
218 return true;215 return true;
219}216}
220
221module.exports = {
222 loadPlugins,
223};
src/polyfill.js+0 -10
@@ -1,10 +0,0 @@
1if (!Array.prototype.findLastIndex) {
2 Array.prototype.findLastIndex = function (callback, thisArg) {
3 for (let i = this.length - 1; i >= 0; i--) {
4 if (callback.call(thisArg, this[i], i, this)) return i;
5 }
6 return -1;
7 };
8}
9
10module.exports = {};
src/prompt-converters.js+10 -22
@@ -1,6 +1,5 @@
1require('./polyfill.js');1import crypto from 'node:crypto';
2const { getConfigValue } = require('./util.js');2import { getConfigValue } from './util.js';
3const crypto = require('crypto');
43
5const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');4const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
65
@@ -17,7 +16,7 @@ const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get start
17 * @returns {string} Prompt for Claude16 * @returns {string} Prompt for Claude
18 * @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).17 * @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).
19 */18 */
20function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg, excludePrefixes) {19export function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg, excludePrefixes) {
2120
22 //Prepare messages for claude.21 //Prepare messages for claude.
23 //When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).22 //When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
@@ -96,7 +95,7 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
96 * @param {string} charName Character name95 * @param {string} charName Character name
97 * @param {string} userName User name96 * @param {string} userName User name
98 */97 */
99function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, humanMsgFix, charName = '', userName = '') {98export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, humanMsgFix, charName = '', userName = '') {
100 let systemPrompt = [];99 let systemPrompt = [];
101 if (useSysPrompt) {100 if (useSysPrompt) {
102 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.101 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@@ -279,7 +278,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools,
279 * @param {string} userName User name278 * @param {string} userName User name
280 * @returns {{chatHistory: object[]}} Prompt for Cohere279 * @returns {{chatHistory: object[]}} Prompt for Cohere
281 */280 */
282function convertCohereMessages(messages, charName = '', userName = '') {281export function convertCohereMessages(messages, charName = '', userName = '') {
283 if (messages.length === 0) {282 if (messages.length === 0) {
284 messages.unshift({283 messages.unshift({
285 role: 'user',284 role: 'user',
@@ -333,7 +332,7 @@ function convertCohereMessages(messages, charName = '', userName = '') {
333 * @param {string} userName User name332 * @param {string} userName User name
334 * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models333 * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models
335 */334 */
336function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = '') {335export function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = '') {
337 // This is a 1x1 transparent PNG336 // This is a 1x1 transparent PNG
338 const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';337 const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
339338
@@ -456,7 +455,7 @@ function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '
456 * @param {string} charName Character name455 * @param {string} charName Character name
457 * @param {string} userName User name456 * @param {string} userName User name
458 */457 */
459function convertAI21Messages(messages, charName = '', userName = '') {458export function convertAI21Messages(messages, charName = '', userName = '') {
460 if (!Array.isArray(messages)) {459 if (!Array.isArray(messages)) {
461 return [];460 return [];
462 }461 }
@@ -528,7 +527,7 @@ function convertAI21Messages(messages, charName = '', userName = '') {
528 * @param {string} charName Character name527 * @param {string} charName Character name
529 * @param {string} userName User name528 * @param {string} userName User name
530 */529 */
531function convertMistralMessages(messages, charName = '', userName = '') {530export function convertMistralMessages(messages, charName = '', userName = '') {
532 if (!Array.isArray(messages)) {531 if (!Array.isArray(messages)) {
533 return [];532 return [];
534 }533 }
@@ -612,7 +611,7 @@ function convertMistralMessages(messages, charName = '', userName = '') {
612 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message611 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
613 * @returns {any[]} Merged messages612 * @returns {any[]} Merged messages
614 */613 */
615function mergeMessages(messages, charName, userName, strict) {614export function mergeMessages(messages, charName, userName, strict) {
616 let mergedMessages = [];615 let mergedMessages = [];
617616
618 // Remove names from the messages617 // Remove names from the messages
@@ -686,7 +685,7 @@ function mergeMessages(messages, charName, userName, strict) {
686 * @param {object[]} messages Array of messages685 * @param {object[]} messages Array of messages
687 * @returns {string} Prompt for Text Completion API686 * @returns {string} Prompt for Text Completion API
688 */687 */
689function convertTextCompletionPrompt(messages) {688export function convertTextCompletionPrompt(messages) {
690 if (typeof messages === 'string') {689 if (typeof messages === 'string') {
691 return messages;690 return messages;
692 }691 }
@@ -705,14 +704,3 @@ function convertTextCompletionPrompt(messages) {
705 });704 });
706 return messageStrings.join('\n') + '\nassistant:';705 return messageStrings.join('\n') + '\nassistant:';
707}706}
708
709module.exports = {
710 convertClaudePrompt,
711 convertClaudeMessages,
712 convertGooglePrompt,
713 convertTextCompletionPrompt,
714 convertCohereMessages,
715 convertMistralMessages,
716 convertAI21Messages,
717 mergeMessages,
718};
src/request-proxy.js+6 -10
@@ -1,7 +1,8 @@
1const http = require('node:http');1import process from 'node:process';
2const https = require('node:https');2import http from 'node:http';
33import https from 'node:https';
4const { isValidUrl, color } = require('./util.js');4import { ProxyAgent } from 'proxy-agent';
5import { isValidUrl, color } from './util.js';
56
6const LOG_HEADER = '[Request Proxy]';7const LOG_HEADER = '[Request Proxy]';
78
@@ -13,10 +14,8 @@ const LOG_HEADER = '[Request Proxy]';
13 * @property {string} url Proxy URL.14 * @property {string} url Proxy URL.
14 * @property {string[]} bypass List of URLs to bypass proxy.15 * @property {string[]} bypass List of URLs to bypass proxy.
15 */16 */
16function initRequestProxy({ enabled, url, bypass }) {17export default function initRequestProxy({ enabled, url, bypass }) {
17 try {18 try {
18 const { ProxyAgent } = require('proxy-agent');
19
20 // No proxy is enabled, so return19 // No proxy is enabled, so return
21 if (!enabled) {20 if (!enabled) {
22 return;21 return;
@@ -36,7 +35,6 @@ function initRequestProxy({ enabled, url, bypass }) {
36 // Reference: https://github.com/Rob--W/proxy-from-env35 // Reference: https://github.com/Rob--W/proxy-from-env
37 process.env.all_proxy = url;36 process.env.all_proxy = url;
3837
39
40 if (Array.isArray(bypass) && bypass.length > 0) {38 if (Array.isArray(bypass) && bypass.length > 0) {
41 process.env.no_proxy = bypass.join(',');39 process.env.no_proxy = bypass.join(',');
42 }40 }
@@ -52,5 +50,3 @@ function initRequestProxy({ enabled, url, bypass }) {
52 console.error(color.red(LOG_HEADER), 'Failed to initialize request proxy:', error);50 console.error(color.red(LOG_HEADER), 'Failed to initialize request proxy:', error);
53 }51 }
54}52}
55
56module.exports = initRequestProxy;
src/transformers.mjs+11 -7
@@ -1,7 +1,10 @@
1import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';1import path from 'node:path';
2import fs from 'node:fs';
3import process from 'node:process';
4import { Buffer } from 'node:buffer';
5
6import { pipeline, env, RawImage } from 'sillytavern-transformers';
2import { getConfigValue } from './util.js';7import { getConfigValue } from './util.js';
3import path from 'path';
4import fs from 'fs';
58
6configureTransformers();9configureTransformers();
710
@@ -50,7 +53,7 @@ const tasks = {
50 * @param {string} image Base64-encoded image53 * @param {string} image Base64-encoded image
51 * @returns {Promise<RawImage|null>} Object representing the image54 * @returns {Promise<RawImage|null>} Object representing the image
52 */55 */
53async function getRawImage(image) {56export async function getRawImage(image) {
54 try {57 try {
55 const buffer = Buffer.from(image, 'base64');58 const buffer = Buffer.from(image, 'base64');
56 const byteArray = new Uint8Array(buffer);59 const byteArray = new Uint8Array(buffer);
@@ -114,9 +117,9 @@ async function migrateCacheToDataDir() {
114 * Gets the transformers.js pipeline for a given task.117 * Gets the transformers.js pipeline for a given task.
115 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for118 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for
116 * @param {string} forceModel The model to use for the pipeline, if any119 * @param {string} forceModel The model to use for the pipeline, if any
117 * @returns {Promise<Pipeline>} Pipeline for the task120 * @returns {Promise<import('sillytavern-transformers').Pipeline>} The transformers.js pipeline
118 */121 */
119async function getPipeline(task, forceModel = '') {122export async function getPipeline(task, forceModel = '') {
120 await migrateCacheToDataDir();123 await migrateCacheToDataDir();
121124
122 if (tasks[task].pipeline) {125 if (tasks[task].pipeline) {
@@ -134,10 +137,11 @@ async function getPipeline(task, forceModel = '') {
134 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
135 tasks[task].pipeline = instance;138 tasks[task].pipeline = instance;
136 tasks[task].currentModel = model;139 tasks[task].currentModel = model;
140 // @ts-ignore
137 return instance;141 return instance;
138}142}
139143
140export default {144export default {
141 getPipeline,
142 getRawImage,145 getRawImage,
146 getPipeline,
143};147};
src/users.js+44 -70
@@ -1,22 +1,25 @@
1// Native Node Modules1// Native Node Modules
2const path = require('path');2import path from 'node:path';
3const fs = require('fs');3import fs from 'node:fs';
4const crypto = require('crypto');4import crypto from 'node:crypto';
5const os = require('os');5import os from 'node:os';
6import process from 'node:process';
7import { Buffer } from 'node:buffer';
68
7// Express and other dependencies9// Express and other dependencies
8const storage = require('node-persist');10import storage from 'node-persist';
9const express = require('express');11import express from 'express';
10const mime = require('mime-types');12import mime from 'mime-types';
11const archiver = require('archiver');13import archiver from 'archiver';
12const writeFileAtomicSync = require('write-file-atomic').sync;14import _ from 'lodash';
13const _ = require('lodash');15import { sync as writeFileAtomicSync } from 'write-file-atomic';
1416
15const { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } = require('./constants');17import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js';
16const { getConfigValue, color, delay, setConfigValue, generateTimestamp } = require('./util');18import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js';
17const { readSecret, writeSecret } = require('./endpoints/secrets');19import { readSecret, writeSecret } from './endpoints/secrets.js';
1820import { getContentOfType } from './endpoints/content-manager.js';
19const KEY_PREFIX = 'user:';21
22export const KEY_PREFIX = 'user:';
20const AVATAR_PREFIX = 'avatar:';23const AVATAR_PREFIX = 'avatar:';
21const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);24const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
22const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false);25const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false);
@@ -92,9 +95,9 @@ const STORAGE_KEYS = {
9295
93/**96/**
94 * Ensures that the content directories exist.97 * Ensures that the content directories exist.
95 * @returns {Promise<import('./users').UserDirectoryList[]>} - The list of user directories98 * @returns {Promise<import('./users.js').UserDirectoryList[]>} - The list of user directories
96 */99 */
97async function ensurePublicDirectoriesExist() {100export async function ensurePublicDirectoriesExist() {
98 for (const dir of Object.values(PUBLIC_DIRECTORIES)) {101 for (const dir of Object.values(PUBLIC_DIRECTORIES)) {
99 if (!fs.existsSync(dir)) {102 if (!fs.existsSync(dir)) {
100 fs.mkdirSync(dir, { recursive: true });103 fs.mkdirSync(dir, { recursive: true });
@@ -115,9 +118,9 @@ async function ensurePublicDirectoriesExist() {
115118
116/**119/**
117 * Gets a list of all user directories.120 * Gets a list of all user directories.
118 * @returns {Promise<import('./users').UserDirectoryList[]>} - The list of user directories121 * @returns {Promise<import('./users.js').UserDirectoryList[]>} - The list of user directories
119 */122 */
120async function getUserDirectoriesList() {123export async function getUserDirectoriesList() {
121 const userHandles = await getAllUserHandles();124 const userHandles = await getAllUserHandles();
122 const directoriesList = userHandles.map(handle => getUserDirectories(handle));125 const directoriesList = userHandles.map(handle => getUserDirectories(handle));
123 return directoriesList;126 return directoriesList;
@@ -126,7 +129,7 @@ async function getUserDirectoriesList() {
126/**129/**
127 * Perform migration from the old user data format to the new one.130 * Perform migration from the old user data format to the new one.
128 */131 */
129async function migrateUserData() {132export async function migrateUserData() {
130 const publicDirectory = path.join(process.cwd(), 'public');133 const publicDirectory = path.join(process.cwd(), 'public');
131134
132 // No need to migrate if the characters directory doesn't exists135 // No need to migrate if the characters directory doesn't exists
@@ -328,14 +331,13 @@ async function migrateUserData() {
328 console.log(color.green('Migration completed!'));331 console.log(color.green('Migration completed!'));
329}332}
330333
331async function migrateSystemPrompts() {334export async function migrateSystemPrompts() {
332 /**335 /**
333 * Gets the default system prompts.336 * Gets the default system prompts.
334 * @returns {Promise<any[]>} - The list of default system prompts337 * @returns {Promise<any[]>} - The list of default system prompts
335 */338 */
336 async function getDefaultSystemPrompts() {339 async function getDefaultSystemPrompts() {
337 try {340 try {
338 const { getContentOfType } = await import('./endpoints/content-manager.js');
339 return getContentOfType('sysprompt', 'json');341 return getContentOfType('sysprompt', 'json');
340 } catch {342 } catch {
341 return [];343 return [];
@@ -391,7 +393,7 @@ async function migrateSystemPrompts() {
391 * @param {string} handle User handle393 * @param {string} handle User handle
392 * @returns {string} The key for the user storage394 * @returns {string} The key for the user storage
393 */395 */
394function toKey(handle) {396export function toKey(handle) {
395 return `${KEY_PREFIX}${handle}`;397 return `${KEY_PREFIX}${handle}`;
396}398}
397399
@@ -400,7 +402,7 @@ function toKey(handle) {
400 * @param {string} handle User handle402 * @param {string} handle User handle
401 * @returns {string} The key for the avatar storage403 * @returns {string} The key for the avatar storage
402 */404 */
403function toAvatarKey(handle) {405export function toAvatarKey(handle) {
404 return `${AVATAR_PREFIX}${handle}`;406 return `${AVATAR_PREFIX}${handle}`;
405}407}
406408
@@ -409,7 +411,7 @@ function toAvatarKey(handle) {
409 * @param {string} dataRoot The root directory for user data411 * @param {string} dataRoot The root directory for user data
410 * @returns {Promise<void>}412 * @returns {Promise<void>}
411 */413 */
412async function initUserStorage(dataRoot) {414export async function initUserStorage(dataRoot) {
413 global.DATA_ROOT = dataRoot;415 global.DATA_ROOT = dataRoot;
414 console.log('Using data root:', color.green(global.DATA_ROOT));416 console.log('Using data root:', color.green(global.DATA_ROOT));
415 console.log();417 console.log();
@@ -430,7 +432,7 @@ async function initUserStorage(dataRoot) {
430 * Get the cookie secret from the config. If it doesn't exist, generate a new one.432 * Get the cookie secret from the config. If it doesn't exist, generate a new one.
431 * @returns {string} The cookie secret433 * @returns {string} The cookie secret
432 */434 */
433function getCookieSecret() {435export function getCookieSecret() {
434 let secret = getConfigValue(STORAGE_KEYS.cookieSecret);436 let secret = getConfigValue(STORAGE_KEYS.cookieSecret);
435437
436 if (!secret) {438 if (!secret) {
@@ -446,7 +448,7 @@ function getCookieSecret() {
446 * Generates a random password salt.448 * Generates a random password salt.
447 * @returns {string} The password salt449 * @returns {string} The password salt
448 */450 */
449function getPasswordSalt() {451export function getPasswordSalt() {
450 return crypto.randomBytes(16).toString('base64');452 return crypto.randomBytes(16).toString('base64');
451}453}
452454
@@ -454,7 +456,7 @@ function getPasswordSalt() {
454 * Get the session name for the current server.456 * Get the session name for the current server.
455 * @returns {string} The session name457 * @returns {string} The session name
456 */458 */
457function getCookieSessionName() {459export function getCookieSessionName() {
458 // Get server hostname and hash it to generate a session suffix460 // Get server hostname and hash it to generate a session suffix
459 const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8);461 const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8);
460 return `session-${suffix}`;462 return `session-${suffix}`;
@@ -466,7 +468,7 @@ function getCookieSessionName() {
466 * @param {string} salt Salt to use for hashing468 * @param {string} salt Salt to use for hashing
467 * @returns {string} Hashed password469 * @returns {string} Hashed password
468 */470 */
469function getPasswordHash(password, salt) {471export function getPasswordHash(password, salt) {
470 return crypto.scryptSync(password.normalize(), salt, 64).toString('base64');472 return crypto.scryptSync(password.normalize(), salt, 64).toString('base64');
471}473}
472474
@@ -475,7 +477,7 @@ function getPasswordHash(password, salt) {
475 * @param {import('express').Request} [request] HTTP request object477 * @param {import('express').Request} [request] HTTP request object
476 * @returns {string} The CSRF secret478 * @returns {string} The CSRF secret
477 */479 */
478function getCsrfSecret(request) {480export function getCsrfSecret(request) {
479 if (!request || !request.user) {481 if (!request || !request.user) {
480 return ANON_CSRF_SECRET;482 return ANON_CSRF_SECRET;
481 }483 }
@@ -494,7 +496,7 @@ function getCsrfSecret(request) {
494 * Gets a list of all user handles.496 * Gets a list of all user handles.
495 * @returns {Promise<string[]>} - The list of user handles497 * @returns {Promise<string[]>} - The list of user handles
496 */498 */
497async function getAllUserHandles() {499export async function getAllUserHandles() {
498 const keys = await storage.keys(x => x.key.startsWith(KEY_PREFIX));500 const keys = await storage.keys(x => x.key.startsWith(KEY_PREFIX));
499 const handles = keys.map(x => x.replace(KEY_PREFIX, ''));501 const handles = keys.map(x => x.replace(KEY_PREFIX, ''));
500 return handles;502 return handles;
@@ -505,7 +507,7 @@ async function getAllUserHandles() {
505 * @param {string} handle User handle507 * @param {string} handle User handle
506 * @returns {UserDirectoryList} User directories508 * @returns {UserDirectoryList} User directories
507 */509 */
508function getUserDirectories(handle) {510export function getUserDirectories(handle) {
509 if (DIRECTORIES_CACHE.has(handle)) {511 if (DIRECTORIES_CACHE.has(handle)) {
510 const cache = DIRECTORIES_CACHE.get(handle);512 const cache = DIRECTORIES_CACHE.get(handle);
511 if (cache) {513 if (cache) {
@@ -526,7 +528,7 @@ function getUserDirectories(handle) {
526 * @param {string} handle User handle528 * @param {string} handle User handle
527 * @returns {Promise<string>} User avatar URL529 * @returns {Promise<string>} User avatar URL
528 */530 */
529async function getUserAvatar(handle) {531export async function getUserAvatar(handle) {
530 try {532 try {
531 // Check if the user has a custom avatar533 // Check if the user has a custom avatar
532 const avatarKey = toAvatarKey(handle);534 const avatarKey = toAvatarKey(handle);
@@ -563,7 +565,7 @@ async function getUserAvatar(handle) {
563 * @param {import('express').Request} request Request object565 * @param {import('express').Request} request Request object
564 * @returns {boolean} Whether the user should be redirected to the login page566 * @returns {boolean} Whether the user should be redirected to the login page
565 */567 */
566function shouldRedirectToLogin(request) {568export function shouldRedirectToLogin(request) {
567 return ENABLE_ACCOUNTS && !request.user;569 return ENABLE_ACCOUNTS && !request.user;
568}570}
569571
@@ -574,7 +576,7 @@ function shouldRedirectToLogin(request) {
574 * @param {boolean} basicAuthMode If Basic auth mode is enabled576 * @param {boolean} basicAuthMode If Basic auth mode is enabled
575 * @returns {Promise<boolean>} Whether auto-login was performed577 * @returns {Promise<boolean>} Whether auto-login was performed
576 */578 */
577async function tryAutoLogin(request, basicAuthMode) {579export async function tryAutoLogin(request, basicAuthMode) {
578 if (!ENABLE_ACCOUNTS || request.user || !request.session) {580 if (!ENABLE_ACCOUNTS || request.user || !request.session) {
579 return false;581 return false;
580 }582 }
@@ -693,7 +695,7 @@ async function basicUserLogin(request) {
693 * @param {import('express').Response} response Response object695 * @param {import('express').Response} response Response object
694 * @param {import('express').NextFunction} next Next function696 * @param {import('express').NextFunction} next Next function
695 */697 */
696async function setUserDataMiddleware(request, response, next) {698export async function setUserDataMiddleware(request, response, next) {
697 // If user accounts are disabled, use the default user699 // If user accounts are disabled, use the default user
698 if (!ENABLE_ACCOUNTS) {700 if (!ENABLE_ACCOUNTS) {
699 const handle = DEFAULT_USER.handle;701 const handle = DEFAULT_USER.handle;
@@ -751,7 +753,7 @@ async function setUserDataMiddleware(request, response, next) {
751 * @param {import('express').Response} response Response object753 * @param {import('express').Response} response Response object
752 * @param {import('express').NextFunction} next Next function754 * @param {import('express').NextFunction} next Next function
753 */755 */
754function requireLoginMiddleware(request, response, next) {756export function requireLoginMiddleware(request, response, next) {
755 if (!request.user) {757 if (!request.user) {
756 return response.sendStatus(403);758 return response.sendStatus(403);
757 }759 }
@@ -787,7 +789,7 @@ function createRouteHandler(directoryFn) {
787 * @param {import('express').NextFunction} next Next function789 * @param {import('express').NextFunction} next Next function
788 * @returns {any}790 * @returns {any}
789 */791 */
790function requireAdminMiddleware(request, response, next) {792export function requireAdminMiddleware(request, response, next) {
791 if (!request.user) {793 if (!request.user) {
792 return response.sendStatus(403);794 return response.sendStatus(403);
793 }795 }
@@ -806,7 +808,7 @@ function requireAdminMiddleware(request, response, next) {
806 * @param {import('express').Response} response Express response object to write to808 * @param {import('express').Response} response Express response object to write to
807 * @returns {Promise<void>} Promise that resolves when the archive is created809 * @returns {Promise<void>} Promise that resolves when the archive is created
808 */810 */
809async function createBackupArchive(handle, response) {811export async function createBackupArchive(handle, response) {
810 const directories = getUserDirectories(handle);812 const directories = getUserDirectories(handle);
811813
812 console.log('Backup requested for', handle);814 console.log('Backup requested for', handle);
@@ -855,7 +857,7 @@ async function getAllUsers() {
855 * Gets all of the enabled users.857 * Gets all of the enabled users.
856 * @returns {Promise<User[]>}858 * @returns {Promise<User[]>}
857 */859 */
858async function getAllEnabledUsers() {860export async function getAllEnabledUsers() {
859 const users = await getAllUsers();861 const users = await getAllUsers();
860 return users.filter(x => x.enabled);862 return users.filter(x => x.enabled);
861}863}
@@ -863,7 +865,7 @@ async function getAllEnabledUsers() {
863/**865/**
864 * Express router for serving files from the user's directories.866 * Express router for serving files from the user's directories.
865 */867 */
866const router = express.Router();868export const router = express.Router();
867router.use('/backgrounds/*', createRouteHandler(req => req.user.directories.backgrounds));869router.use('/backgrounds/*', createRouteHandler(req => req.user.directories.backgrounds));
868router.use('/characters/*', createRouteHandler(req => req.user.directories.characters));870router.use('/characters/*', createRouteHandler(req => req.user.directories.characters));
869router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.avatars));871router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.avatars));
@@ -871,31 +873,3 @@ router.use('/assets/*', createRouteHandler(req => req.user.directories.assets));
871router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));873router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));
872router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));874router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
873router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));875router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
874
875module.exports = {
876 KEY_PREFIX,
877 toKey,
878 toAvatarKey,
879 initUserStorage,
880 ensurePublicDirectoriesExist,
881 getUserDirectoriesList,
882 getAllUserHandles,
883 getUserDirectories,
884 setUserDataMiddleware,
885 requireLoginMiddleware,
886 requireAdminMiddleware,
887 migrateUserData,
888 migrateSystemPrompts,
889 getPasswordSalt,
890 getPasswordHash,
891 getCsrfSecret,
892 getCookieSecret,
893 getCookieSessionName,
894 getUserAvatar,
895 shouldRedirectToLogin,
896 createBackupArchive,
897 tryAutoLogin,
898 getAllUsers,
899 getAllEnabledUsers,
900 router,
901};
src/util.js+59 -81
@@ -1,13 +1,18 @@
1const path = require('path');1import path from 'node:path';
2const fs = require('fs');2import fs from 'node:fs';
3const commandExistsSync = require('command-exists').sync;3import http2 from 'node:http2';
4const writeFileAtomicSync = require('write-file-atomic').sync;4import process from 'node:process';
5const _ = require('lodash');5import { Readable } from 'node:stream';
6const yauzl = require('yauzl');6import { createRequire } from 'node:module';
7const mime = require('mime-types');7import { Buffer } from 'node:buffer';
8const yaml = require('yaml');8
9const { default: simpleGit } = require('simple-git');9import yaml from 'yaml';
10const { Readable } = require('stream');10import { sync as commandExistsSync } from 'command-exists';
11import { sync as writeFileAtomicSync } from 'write-file-atomic';
12import _ from 'lodash';
13import yauzl from 'yauzl';
14import mime from 'mime-types';
15import { default as simpleGit } from 'simple-git';
1116
12/**17/**
13 * Parsed config object.18 * Parsed config object.
@@ -18,7 +23,7 @@ let CACHED_CONFIG = null;
18 * Returns the config object from the config.yaml file.23 * Returns the config object from the config.yaml file.
19 * @returns {object} Config object24 * @returns {object} Config object
20 */25 */
21function getConfig() {26export function getConfig() {
22 if (CACHED_CONFIG) {27 if (CACHED_CONFIG) {
23 return CACHED_CONFIG;28 return CACHED_CONFIG;
24 }29 }
@@ -46,7 +51,7 @@ function getConfig() {
46 * @param {any} defaultValue - Default value to return if the key is not found51 * @param {any} defaultValue - Default value to return if the key is not found
47 * @returns {any} Value for the given key52 * @returns {any} Value for the given key
48 */53 */
49function getConfigValue(key, defaultValue = null) {54export function getConfigValue(key, defaultValue = null) {
50 const config = getConfig();55 const config = getConfig();
51 return _.get(config, key, defaultValue);56 return _.get(config, key, defaultValue);
52}57}
@@ -56,7 +61,7 @@ function getConfigValue(key, defaultValue = null) {
56 * @param {string} key Key to set61 * @param {string} key Key to set
57 * @param {any} value Value to set62 * @param {any} value Value to set
58 */63 */
59function setConfigValue(key, value) {64export function setConfigValue(key, value) {
60 // Reset cache so that the next getConfig call will read the updated config file65 // Reset cache so that the next getConfig call will read the updated config file
61 CACHED_CONFIG = null;66 CACHED_CONFIG = null;
62 const config = getConfig();67 const config = getConfig();
@@ -69,7 +74,7 @@ function setConfigValue(key, value) {
69 * @param {string} auth username:password74 * @param {string} auth username:password
70 * @returns {string} Basic Auth header value75 * @returns {string} Basic Auth header value
71 */76 */
72function getBasicAuthHeader(auth) {77export function getBasicAuthHeader(auth) {
73 const encoded = Buffer.from(`${auth}`).toString('base64');78 const encoded = Buffer.from(`${auth}`).toString('base64');
74 return `Basic ${encoded}`;79 return `Basic ${encoded}`;
75}80}
@@ -79,7 +84,7 @@ function getBasicAuthHeader(auth) {
79 * Also returns the agent string for the Horde API.84 * Also returns the agent string for the Horde API.
80 * @returns {Promise<{agent: string, pkgVersion: string, gitRevision: string | null, gitBranch: string | null, commitDate: string | null, isLatest: boolean}>} Version info object85 * @returns {Promise<{agent: string, pkgVersion: string, gitRevision: string | null, gitBranch: string | null, commitDate: string | null, isLatest: boolean}>} Version info object
81 */86 */
82async function getVersion() {87export async function getVersion() {
83 let pkgVersion = 'UNKNOWN';88 let pkgVersion = 'UNKNOWN';
84 let gitRevision = null;89 let gitRevision = null;
85 let gitBranch = null;90 let gitBranch = null;
@@ -87,9 +92,10 @@ async function getVersion() {
87 let isLatest = true;92 let isLatest = true;
8893
89 try {94 try {
95 const require = createRequire(import.meta.url);
90 const pkgJson = require(path.join(process.cwd(), './package.json'));96 const pkgJson = require(path.join(process.cwd(), './package.json'));
91 pkgVersion = pkgJson.version;97 pkgVersion = pkgJson.version;
92 if (!process['pkg'] && commandExistsSync('git')) {98 if (commandExistsSync('git')) {
93 const git = simpleGit();99 const git = simpleGit();
94 const cwd = process.cwd();100 const cwd = process.cwd();
95 gitRevision = await git.cwd(cwd).revparse(['--short', 'HEAD']);101 gitRevision = await git.cwd(cwd).revparse(['--short', 'HEAD']);
@@ -117,7 +123,7 @@ async function getVersion() {
117 * @param {number} ms Milliseconds to wait123 * @param {number} ms Milliseconds to wait
118 * @returns {Promise<void>} Promise that resolves after the given amount of milliseconds124 * @returns {Promise<void>} Promise that resolves after the given amount of milliseconds
119 */125 */
120function delay(ms) {126export function delay(ms) {
121 return new Promise(resolve => setTimeout(resolve, ms));127 return new Promise(resolve => setTimeout(resolve, ms));
122}128}
123129
@@ -127,7 +133,7 @@ function delay(ms) {
127 * @returns {string} Random hex string133 * @returns {string} Random hex string
128 * @example getHexString(8) // 'a1b2c3d4'134 * @example getHexString(8) // 'a1b2c3d4'
129 */135 */
130function getHexString(length) {136export function getHexString(length) {
131 const chars = '0123456789abcdef';137 const chars = '0123456789abcdef';
132 let result = '';138 let result = '';
133 for (let i = 0; i < length; i++) {139 for (let i = 0; i < length; i++) {
@@ -142,7 +148,7 @@ function getHexString(length) {
142 * @param {string} fileExtension File extension to look for148 * @param {string} fileExtension File extension to look for
143 * @returns {Promise<Buffer|null>} Buffer containing the extracted file. Null if the file was not found.149 * @returns {Promise<Buffer|null>} Buffer containing the extracted file. Null if the file was not found.
144 */150 */
145async function extractFileFromZipBuffer(archiveBuffer, fileExtension) {151export async function extractFileFromZipBuffer(archiveBuffer, fileExtension) {
146 return await new Promise((resolve, reject) => yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => {152 return await new Promise((resolve, reject) => yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => {
147 if (err) {153 if (err) {
148 reject(err);154 reject(err);
@@ -181,7 +187,7 @@ async function extractFileFromZipBuffer(archiveBuffer, fileExtension) {
181 * @param {string} zipFilePath Path to the ZIP archive187 * @param {string} zipFilePath Path to the ZIP archive
182 * @returns {Promise<[string, Buffer][]>} Array of image buffers188 * @returns {Promise<[string, Buffer][]>} Array of image buffers
183 */189 */
184async function getImageBuffers(zipFilePath) {190export async function getImageBuffers(zipFilePath) {
185 return new Promise((resolve, reject) => {191 return new Promise((resolve, reject) => {
186 // Check if the zip file exists192 // Check if the zip file exists
187 if (!fs.existsSync(zipFilePath)) {193 if (!fs.existsSync(zipFilePath)) {
@@ -237,7 +243,7 @@ async function getImageBuffers(zipFilePath) {
237 * @param {any} readableStream Readable stream to read from243 * @param {any} readableStream Readable stream to read from
238 * @returns {Promise<Buffer[]>} Array of chunks244 * @returns {Promise<Buffer[]>} Array of chunks
239 */245 */
240async function readAllChunks(readableStream) {246export async function readAllChunks(readableStream) {
241 return new Promise((resolve, reject) => {247 return new Promise((resolve, reject) => {
242 // Consume the readable stream248 // Consume the readable stream
243 const chunks = [];249 const chunks = [];
@@ -261,7 +267,7 @@ function isObject(item) {
261 return (item && typeof item === 'object' && !Array.isArray(item));267 return (item && typeof item === 'object' && !Array.isArray(item));
262}268}
263269
264function deepMerge(target, source) {270export function deepMerge(target, source) {
265 let output = Object.assign({}, target);271 let output = Object.assign({}, target);
266 if (isObject(target) && isObject(source)) {272 if (isObject(target) && isObject(source)) {
267 Object.keys(source).forEach(key => {273 Object.keys(source).forEach(key => {
@@ -278,7 +284,7 @@ function deepMerge(target, source) {
278 return output;284 return output;
279}285}
280286
281const color = {287export const color = {
282 byNum: (mess, fgNum) => {288 byNum: (mess, fgNum) => {
283 mess = mess || '';289 mess = mess || '';
284 fgNum = fgNum === undefined ? 31 : fgNum;290 fgNum = fgNum === undefined ? 31 : fgNum;
@@ -298,7 +304,7 @@ const color = {
298 * Gets a random UUIDv4 string.304 * Gets a random UUIDv4 string.
299 * @returns {string} A UUIDv4 string305 * @returns {string} A UUIDv4 string
300 */306 */
301function uuidv4() {307export function uuidv4() {
302 if ('crypto' in global && 'randomUUID' in global.crypto) {308 if ('crypto' in global && 'randomUUID' in global.crypto) {
303 return global.crypto.randomUUID();309 return global.crypto.randomUUID();
304 }310 }
@@ -309,7 +315,7 @@ function uuidv4() {
309 });315 });
310}316}
311317
312function humanizedISO8601DateTime(date) {318export function humanizedISO8601DateTime(date) {
313 let baseDate = typeof date === 'number' ? new Date(date) : new Date();319 let baseDate = typeof date === 'number' ? new Date(date) : new Date();
314 let humanYear = baseDate.getFullYear();320 let humanYear = baseDate.getFullYear();
315 let humanMonth = (baseDate.getMonth() + 1);321 let humanMonth = (baseDate.getMonth() + 1);
@@ -322,7 +328,7 @@ function humanizedISO8601DateTime(date) {
322 return HumanizedDateTime;328 return HumanizedDateTime;
323}329}
324330
325function tryParse(str) {331export function tryParse(str) {
326 try {332 try {
327 return JSON.parse(str);333 return JSON.parse(str);
328 } catch {334 } catch {
@@ -337,7 +343,7 @@ function tryParse(str) {
337 * @param {string} inputPath The path to be converted.343 * @param {string} inputPath The path to be converted.
338 * @returns The relative URL path from which the client can access the file.344 * @returns The relative URL path from which the client can access the file.
339 */345 */
340function clientRelativePath(root, inputPath) {346export function clientRelativePath(root, inputPath) {
341 if (!inputPath.startsWith(root)) {347 if (!inputPath.startsWith(root)) {
342 throw new Error('Input path does not start with the root directory');348 throw new Error('Input path does not start with the root directory');
343 }349 }
@@ -350,11 +356,11 @@ function clientRelativePath(root, inputPath) {
350 * @param {string} filename The file name to remove the extension from.356 * @param {string} filename The file name to remove the extension from.
351 * @returns The file name, sans extension357 * @returns The file name, sans extension
352 */358 */
353function removeFileExtension(filename) {359export function removeFileExtension(filename) {
354 return filename.replace(/\.[^.]+$/, '');360 return filename.replace(/\.[^.]+$/, '');
355}361}
356362
357function generateTimestamp() {363export function generateTimestamp() {
358 const now = new Date();364 const now = new Date();
359 const year = now.getFullYear();365 const year = now.getFullYear();
360 const month = String(now.getMonth() + 1).padStart(2, '0');366 const month = String(now.getMonth() + 1).padStart(2, '0');
@@ -371,7 +377,7 @@ function generateTimestamp() {
371 * @param {string} directory The root directory to remove backups from.377 * @param {string} directory The root directory to remove backups from.
372 * @param {string} prefix File prefix to filter backups by.378 * @param {string} prefix File prefix to filter backups by.
373 */379 */
374function removeOldBackups(directory, prefix) {380export function removeOldBackups(directory, prefix) {
375 const MAX_BACKUPS = Number(getConfigValue('numberOfBackups', 50));381 const MAX_BACKUPS = Number(getConfigValue('numberOfBackups', 50));
376382
377 let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));383 let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
@@ -389,7 +395,7 @@ function removeOldBackups(directory, prefix) {
389 * @param {'name' | 'date'} sortBy Sort images by name or date395 * @param {'name' | 'date'} sortBy Sort images by name or date
390 * @returns {string[]} List of image file names396 * @returns {string[]} List of image file names
391 */397 */
392function getImages(directoryPath, sortBy = 'name') {398export function getImages(directoryPath, sortBy = 'name') {
393 function getSortFunction() {399 function getSortFunction() {
394 switch (sortBy) {400 switch (sortBy) {
395 case 'name':401 case 'name':
@@ -415,7 +421,7 @@ function getImages(directoryPath, sortBy = 'name') {
415 * @param {import('node-fetch').Response} from The Fetch API response to pipe from.421 * @param {import('node-fetch').Response} from The Fetch API response to pipe from.
416 * @param {import('express').Response} to The Express response to pipe to.422 * @param {import('express').Response} to The Express response to pipe to.
417 */423 */
418function forwardFetchResponse(from, to) {424export function forwardFetchResponse(from, to) {
419 let statusCode = from.status;425 let statusCode = from.status;
420 let statusText = from.statusText;426 let statusText = from.statusText;
421427
@@ -434,17 +440,22 @@ function forwardFetchResponse(from, to) {
434440
435 to.statusCode = statusCode;441 to.statusCode = statusCode;
436 to.statusMessage = statusText;442 to.statusMessage = statusText;
437 from.body.pipe(to);
438443
439 to.socket.on('close', function () {444 if (from.body && to.socket) {
440 if (from.body instanceof Readable) from.body.destroy(); // Close the remote stream445 from.body.pipe(to);
441 to.end(); // End the Express response446
442 });447 to.socket.on('close', function () {
448 if (from.body instanceof Readable) from.body.destroy(); // Close the remote stream
449 to.end(); // End the Express response
450 });
443451
444 from.body.on('end', function () {452 from.body.on('end', function () {
445 console.log('Streaming request finished');453 console.log('Streaming request finished');
454 to.end();
455 });
456 } else {
446 to.end();457 to.end();
447 });458 }
448}459}
449460
450/**461/**
@@ -457,10 +468,9 @@ function forwardFetchResponse(from, to) {
457 * @param {object} headers Request headers468 * @param {object} headers Request headers
458 * @returns {Promise<string>} Response body469 * @returns {Promise<string>} Response body
459 */470 */
460function makeHttp2Request(endpoint, method, body, headers) {471export function makeHttp2Request(endpoint, method, body, headers) {
461 return new Promise((resolve, reject) => {472 return new Promise((resolve, reject) => {
462 try {473 try {
463 const http2 = require('http2');
464 const url = new URL(endpoint);474 const url = new URL(endpoint);
465 const client = http2.connect(url.origin);475 const client = http2.connect(url.origin);
466476
@@ -511,7 +521,7 @@ function makeHttp2Request(endpoint, method, body, headers) {
511 * @param {string} yamlString YAML-serialized object521 * @param {string} yamlString YAML-serialized object
512 * @returns522 * @returns
513 */523 */
514function mergeObjectWithYaml(obj, yamlString) {524export function mergeObjectWithYaml(obj, yamlString) {
515 if (!yamlString) {525 if (!yamlString) {
516 return;526 return;
517 }527 }
@@ -540,7 +550,7 @@ function mergeObjectWithYaml(obj, yamlString) {
540 * @param {string} yamlString YAML-serialized array550 * @param {string} yamlString YAML-serialized array
541 * @returns {void} Nothing551 * @returns {void} Nothing
542 */552 */
543function excludeKeysByYaml(obj, yamlString) {553export function excludeKeysByYaml(obj, yamlString) {
544 if (!yamlString) {554 if (!yamlString) {
545 return;555 return;
546 }556 }
@@ -569,14 +579,14 @@ function excludeKeysByYaml(obj, yamlString) {
569 * @param {string} str Input string579 * @param {string} str Input string
570 * @returns {string} Trimmed string580 * @returns {string} Trimmed string
571 */581 */
572function trimV1(str) {582export function trimV1(str) {
573 return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, '');583 return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, '');
574}584}
575585
576/**586/**
577 * Simple TTL memory cache.587 * Simple TTL memory cache.
578 */588 */
579class Cache {589export class Cache {
580 /**590 /**
581 * @param {number} ttl Time to live in milliseconds591 * @param {number} ttl Time to live in milliseconds
582 */592 */
@@ -633,7 +643,7 @@ class Cache {
633 * @param {string} text Text with color formatting643 * @param {string} text Text with color formatting
634 * @returns {string} Text without color formatting644 * @returns {string} Text without color formatting
635 */645 */
636function removeColorFormatting(text) {646export function removeColorFormatting(text) {
637 // ANSI escape codes for colors are usually in the format \x1b[<codes>m647 // ANSI escape codes for colors are usually in the format \x1b[<codes>m
638 return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, '');648 return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, '');
639}649}
@@ -643,7 +653,7 @@ function removeColorFormatting(text) {
643 * @param {number} n Number of times to repeat the separator653 * @param {number} n Number of times to repeat the separator
644 * @returns {string} Separator string654 * @returns {string} Separator string
645 */655 */
646function getSeparator(n) {656export function getSeparator(n) {
647 return '='.repeat(n);657 return '='.repeat(n);
648}658}
649659
@@ -652,7 +662,7 @@ function getSeparator(n) {
652 * @param {string} url String to check662 * @param {string} url String to check
653 * @returns {boolean} If the URL is valid663 * @returns {boolean} If the URL is valid
654 */664 */
655function isValidUrl(url) {665export function isValidUrl(url) {
656 try {666 try {
657 new URL(url);667 new URL(url);
658 return true;668 return true;
@@ -660,35 +670,3 @@ function isValidUrl(url) {
660 return false;670 return false;
661 }671 }
662}672}
663
664module.exports = {
665 getConfig,
666 getConfigValue,
667 setConfigValue,
668 getVersion,
669 getBasicAuthHeader,
670 extractFileFromZipBuffer,
671 getImageBuffers,
672 readAllChunks,
673 delay,
674 deepMerge,
675 color,
676 uuidv4,
677 humanizedISO8601DateTime,
678 tryParse,
679 clientRelativePath,
680 removeFileExtension,
681 generateTimestamp,
682 removeOldBackups,
683 getImages,
684 forwardFetchResponse,
685 getHexString,
686 mergeObjectWithYaml,
687 excludeKeysByYaml,
688 trimV1,
689 Cache,
690 makeHttp2Request,
691 removeColorFormatting,
692 getSeparator,
693 isValidUrl,
694};
src/validator/TavernCardValidator.js+1 -3
@@ -5,7 +5,7 @@
5 *5 *
6 * @link https://github.com/malfoyslastname/character-card-spec-v26 * @link https://github.com/malfoyslastname/character-card-spec-v2
7 */7 */
8class TavernCardValidator {8export class TavernCardValidator {
9 /**9 /**
10 * @type {string|null}10 * @type {string|null}
11 */11 */
@@ -167,5 +167,3 @@ class TavernCardValidator {
167 return true;167 return true;
168 }168 }
169}169}
170
171module.exports = { TavernCardValidator };
src/vectors/cohere-vectors.js+7 -10
@@ -1,15 +1,15 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { SECRET_KEYS, readSecret } = require('../endpoints/secrets');2import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';
33
4/**4/**
5 * Gets the vector for the given text batch from an OpenAI compatible endpoint.5 * Gets the vector for the given text batch from an OpenAI compatible endpoint.
6 * @param {string[]} texts - The array of texts to get the vector for6 * @param {string[]} texts - The array of texts to get the vector for
7 * @param {boolean} isQuery - If the text is a query for embedding search7 * @param {boolean} isQuery - If the text is a query for embedding search
8 * @param {import('../users').UserDirectoryList} directories - The directories object for the user8 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
9 * @param {string} model - The model to use for the embedding9 * @param {string} model - The model to use for the embedding
10 * @returns {Promise<number[][]>} - The array of vectors for the texts10 * @returns {Promise<number[][]>} - The array of vectors for the texts
11 */11 */
12async function getCohereBatchVector(texts, isQuery, directories, model) {12export async function getCohereBatchVector(texts, isQuery, directories, model) {
13 const key = readSecret(directories, SECRET_KEYS.COHERE);13 const key = readSecret(directories, SECRET_KEYS.COHERE);
1414
15 if (!key) {15 if (!key) {
@@ -38,6 +38,7 @@ async function getCohereBatchVector(texts, isQuery, directories, model) {
38 throw new Error('API request failed');38 throw new Error('API request failed');
39 }39 }
4040
41 /** @type {any} */
41 const data = await response.json();42 const data = await response.json();
42 if (!Array.isArray(data?.embeddings?.float)) {43 if (!Array.isArray(data?.embeddings?.float)) {
43 console.log('API response was not an array');44 console.log('API response was not an array');
@@ -51,16 +52,12 @@ async function getCohereBatchVector(texts, isQuery, directories, model) {
51 * Gets the vector for the given text from an OpenAI compatible endpoint.52 * Gets the vector for the given text from an OpenAI compatible endpoint.
52 * @param {string} text - The text to get the vector for53 * @param {string} text - The text to get the vector for
53 * @param {boolean} isQuery - If the text is a query for embedding search54 * @param {boolean} isQuery - If the text is a query for embedding search
54 * @param {import('../users').UserDirectoryList} directories - The directories object for the user55 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
55 * @param {string} model - The model to use for the embedding56 * @param {string} model - The model to use for the embedding
56 * @returns {Promise<number[]>} - The vector for the text57 * @returns {Promise<number[]>} - The vector for the text
57 */58 */
58async function getCohereVector(text, isQuery, directories, model) {59export async function getCohereVector(text, isQuery, directories, model) {
59 const vectors = await getCohereBatchVector([text], isQuery, directories, model);60 const vectors = await getCohereBatchVector([text], isQuery, directories, model);
60 return vectors[0];61 return vectors[0];
61}62}
6263
63module.exports = {
64 getCohereBatchVector,
65 getCohereVector,
66};
src/vectors/embedding.js+4 -9
@@ -1,3 +1,4 @@
1import { getPipeline } from '../transformers.mjs';
1const TASK = 'feature-extraction';2const TASK = 'feature-extraction';
23
3/**4/**
@@ -5,9 +6,8 @@ const TASK = 'feature-extraction';
5 * @param {string} text - The text to vectorize6 * @param {string} text - The text to vectorize
6 * @returns {Promise<number[]>} - The vectorized text in form of an array of numbers7 * @returns {Promise<number[]>} - The vectorized text in form of an array of numbers
7 */8 */
8async function getTransformersVector(text) {9export async function getTransformersVector(text) {
9 const module = await import('../transformers.mjs');10 const pipe = await getPipeline(TASK);
10 const pipe = await module.default.getPipeline(TASK);
11 const result = await pipe(text, { pooling: 'mean', normalize: true });11 const result = await pipe(text, { pooling: 'mean', normalize: true });
12 const vector = Array.from(result.data);12 const vector = Array.from(result.data);
13 return vector;13 return vector;
@@ -18,15 +18,10 @@ async function getTransformersVector(text) {
18 * @param {string[]} texts - The texts to vectorize18 * @param {string[]} texts - The texts to vectorize
19 * @returns {Promise<number[][]>} - The vectorized texts in form of an array of arrays of numbers19 * @returns {Promise<number[][]>} - The vectorized texts in form of an array of arrays of numbers
20 */20 */
21async function getTransformersBatchVector(texts) {21export async function getTransformersBatchVector(texts) {
22 const result = [];22 const result = [];
23 for (const text of texts) {23 for (const text of texts) {
24 result.push(await getTransformersVector(text));24 result.push(await getTransformersVector(text));
25 }25 }
26 return result;26 return result;
27}27}
28
29module.exports = {
30 getTransformersVector,
31 getTransformersBatchVector,
32};
src/vectors/extras-vectors.js+4 -8
@@ -1,4 +1,4 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
22
3/**3/**
4 * Gets the vector for the given text from SillyTavern-extras4 * Gets the vector for the given text from SillyTavern-extras
@@ -7,7 +7,7 @@ const fetch = require('node-fetch').default;
7 * @param {string} apiKey - The Extras API key, or empty string if API key not enabled7 * @param {string} apiKey - The Extras API key, or empty string if API key not enabled
8 * @returns {Promise<number[][]>} - The array of vectors for the texts8 * @returns {Promise<number[][]>} - The array of vectors for the texts
9 */9 */
10async function getExtrasBatchVector(texts, apiUrl, apiKey) {10export async function getExtrasBatchVector(texts, apiUrl, apiKey) {
11 return getExtrasVectorImpl(texts, apiUrl, apiKey);11 return getExtrasVectorImpl(texts, apiUrl, apiKey);
12}12}
1313
@@ -18,7 +18,7 @@ async function getExtrasBatchVector(texts, apiUrl, apiKey) {
18 * @param {string} apiKey - The Extras API key, or empty string if API key not enabled18 * @param {string} apiKey - The Extras API key, or empty string if API key not enabled
19 * @returns {Promise<number[]>} - The vector for the text19 * @returns {Promise<number[]>} - The vector for the text
20 */20 */
21async function getExtrasVector(text, apiUrl, apiKey) {21export async function getExtrasVector(text, apiUrl, apiKey) {
22 return getExtrasVectorImpl(text, apiUrl, apiKey);22 return getExtrasVectorImpl(text, apiUrl, apiKey);
23}23}
2424
@@ -66,13 +66,9 @@ async function getExtrasVectorImpl(text, apiUrl, apiKey) {
66 throw new Error('Extras request failed');66 throw new Error('Extras request failed');
67 }67 }
6868
69 /** @type {any} */
69 const data = await response.json();70 const data = await response.json();
70 const vector = data.embedding; // `embedding`: number[] (one text item), or number[][] (multiple text items).71 const vector = data.embedding; // `embedding`: number[] (one text item), or number[][] (multiple text items).
7172
72 return vector;73 return vector;
73}74}
74
75module.exports = {
76 getExtrasVector,
77 getExtrasBatchVector,
78};
src/vectors/llamacpp-vectors.js+8 -12
@@ -1,15 +1,15 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { setAdditionalHeadersByType } = require('../additional-headers');2import { setAdditionalHeadersByType } from '../additional-headers.js';
3const { TEXTGEN_TYPES } = require('../constants');3import { TEXTGEN_TYPES } from '../constants.js';
44
5/**5/**
6 * Gets the vector for the given text from LlamaCpp6 * Gets the vector for the given text from LlamaCpp
7 * @param {string[]} texts - The array of texts to get the vectors for7 * @param {string[]} texts - The array of texts to get the vectors for
8 * @param {string} apiUrl - The API URL8 * @param {string} apiUrl - The API URL
9 * @param {import('../users').UserDirectoryList} directories - The directories object for the user9 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
10 * @returns {Promise<number[][]>} - The array of vectors for the texts10 * @returns {Promise<number[][]>} - The array of vectors for the texts
11 */11 */
12async function getLlamaCppBatchVector(texts, apiUrl, directories) {12export async function getLlamaCppBatchVector(texts, apiUrl, directories) {
13 const url = new URL(apiUrl);13 const url = new URL(apiUrl);
14 url.pathname = '/v1/embeddings';14 url.pathname = '/v1/embeddings';
1515
@@ -30,6 +30,7 @@ async function getLlamaCppBatchVector(texts, apiUrl, directories) {
30 throw new Error(`LlamaCpp: Failed to get vector for text: ${response.statusText} ${responseText}`);30 throw new Error(`LlamaCpp: Failed to get vector for text: ${response.statusText} ${responseText}`);
31 }31 }
3232
33 /** @type {any} */
33 const data = await response.json();34 const data = await response.json();
3435
35 if (!Array.isArray(data?.data)) {36 if (!Array.isArray(data?.data)) {
@@ -47,15 +48,10 @@ async function getLlamaCppBatchVector(texts, apiUrl, directories) {
47 * Gets the vector for the given text from LlamaCpp48 * Gets the vector for the given text from LlamaCpp
48 * @param {string} text - The text to get the vector for49 * @param {string} text - The text to get the vector for
49 * @param {string} apiUrl - The API URL50 * @param {string} apiUrl - The API URL
50 * @param {import('../users').UserDirectoryList} directories - The directories object for the user51 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
51 * @returns {Promise<number[]>} - The vector for the text52 * @returns {Promise<number[]>} - The vector for the text
52 */53 */
53async function getLlamaCppVector(text, apiUrl, directories) {54export async function getLlamaCppVector(text, apiUrl, directories) {
54 const vectors = await getLlamaCppBatchVector([text], apiUrl, directories);55 const vectors = await getLlamaCppBatchVector([text], apiUrl, directories);
55 return vectors[0];56 return vectors[0];
56}57}
57
58module.exports = {
59 getLlamaCppBatchVector,
60 getLlamaCppVector,
61};
src/vectors/makersuite-vectors.js+7 -11
@@ -1,14 +1,14 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { SECRET_KEYS, readSecret } = require('../endpoints/secrets');2import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';
3const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';3const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
44
5/**5/**
6 * Gets the vector for the given text from gecko model6 * Gets the vector for the given text from gecko model
7 * @param {string[]} texts - The array of texts to get the vector for7 * @param {string[]} texts - The array of texts to get the vector for
8 * @param {import('../users').UserDirectoryList} directories - The directories object for the user8 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
9 * @returns {Promise<number[][]>} - The array of vectors for the texts9 * @returns {Promise<number[][]>} - The array of vectors for the texts
10 */10 */
11async function getMakerSuiteBatchVector(texts, directories) {11export async function getMakerSuiteBatchVector(texts, directories) {
12 const promises = texts.map(text => getMakerSuiteVector(text, directories));12 const promises = texts.map(text => getMakerSuiteVector(text, directories));
13 return await Promise.all(promises);13 return await Promise.all(promises);
14}14}
@@ -16,10 +16,10 @@ async function getMakerSuiteBatchVector(texts, directories) {
16/**16/**
17 * Gets the vector for the given text from Gemini API text-embedding-004 model17 * Gets the vector for the given text from Gemini API text-embedding-004 model
18 * @param {string} text - The text to get the vector for18 * @param {string} text - The text to get the vector for
19 * @param {import('../users').UserDirectoryList} directories - The directories object for the user19 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
20 * @returns {Promise<number[]>} - The vector for the text20 * @returns {Promise<number[]>} - The vector for the text
21 */21 */
22async function getMakerSuiteVector(text, directories) {22export async function getMakerSuiteVector(text, directories) {
23 const key = readSecret(directories, SECRET_KEYS.MAKERSUITE);23 const key = readSecret(directories, SECRET_KEYS.MAKERSUITE);
2424
25 if (!key) {25 if (!key) {
@@ -52,12 +52,8 @@ async function getMakerSuiteVector(text, directories) {
52 throw new Error('Google AI Studio request failed');52 throw new Error('Google AI Studio request failed');
53 }53 }
5454
55 /** @type {any} */
55 const data = await response.json();56 const data = await response.json();
56 // noinspection JSValidateTypes57 // noinspection JSValidateTypes
57 return data['embedding']['values'];58 return data['embedding']['values'];
58}59}
59
60module.exports = {
61 getMakerSuiteVector,
62 getMakerSuiteBatchVector,
63};
src/vectors/nomicai-vectors.js+7 -11
@@ -1,5 +1,5 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { SECRET_KEYS, readSecret } = require('../endpoints/secrets');2import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';
33
4const SOURCES = {4const SOURCES = {
5 'nomicai': {5 'nomicai': {
@@ -13,10 +13,10 @@ const SOURCES = {
13 * Gets the vector for the given text batch from an OpenAI compatible endpoint.13 * Gets the vector for the given text batch from an OpenAI compatible endpoint.
14 * @param {string[]} texts - The array of texts to get the vector for14 * @param {string[]} texts - The array of texts to get the vector for
15 * @param {string} source - The source of the vector15 * @param {string} source - The source of the vector
16 * @param {import('../users').UserDirectoryList} directories - The directories object for the user16 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
17 * @returns {Promise<number[][]>} - The array of vectors for the texts17 * @returns {Promise<number[][]>} - The array of vectors for the texts
18 */18 */
19async function getNomicAIBatchVector(texts, source, directories) {19export async function getNomicAIBatchVector(texts, source, directories) {
20 const config = SOURCES[source];20 const config = SOURCES[source];
2121
22 if (!config) {22 if (!config) {
@@ -51,6 +51,7 @@ async function getNomicAIBatchVector(texts, source, directories) {
51 throw new Error('API request failed');51 throw new Error('API request failed');
52 }52 }
5353
54 /** @type {any} */
54 const data = await response.json();55 const data = await response.json();
55 if (!Array.isArray(data?.embeddings)) {56 if (!Array.isArray(data?.embeddings)) {
56 console.log('API response was not an array');57 console.log('API response was not an array');
@@ -64,15 +65,10 @@ async function getNomicAIBatchVector(texts, source, directories) {
64 * Gets the vector for the given text from an OpenAI compatible endpoint.65 * Gets the vector for the given text from an OpenAI compatible endpoint.
65 * @param {string} text - The text to get the vector for66 * @param {string} text - The text to get the vector for
66 * @param {string} source - The source of the vector67 * @param {string} source - The source of the vector
67 * @param {import('../users').UserDirectoryList} directories - The directories object for the user68 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
68 * @returns {Promise<number[]>} - The vector for the text69 * @returns {Promise<number[]>} - The vector for the text
69 */70 */
70async function getNomicAIVector(text, source, directories) {71export async function getNomicAIVector(text, source, directories) {
71 const vectors = await getNomicAIBatchVector([text], source, directories);72 const vectors = await getNomicAIBatchVector([text], source, directories);
72 return vectors[0];73 return vectors[0];
73}74}
74
75module.exports = {
76 getNomicAIVector,
77 getNomicAIBatchVector,
78};
src/vectors/ollama-vectors.js+8 -12
@@ -1,6 +1,6 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { setAdditionalHeadersByType } = require('../additional-headers');2import { setAdditionalHeadersByType } from '../additional-headers.js';
3const { TEXTGEN_TYPES } = require('../constants');3import { TEXTGEN_TYPES } from '../constants.js';
44
5/**5/**
6 * Gets the vector for the given text from Ollama6 * Gets the vector for the given text from Ollama
@@ -8,10 +8,10 @@ const { TEXTGEN_TYPES } = require('../constants');
8 * @param {string} apiUrl - The API URL8 * @param {string} apiUrl - The API URL
9 * @param {string} model - The model to use9 * @param {string} model - The model to use
10 * @param {boolean} keep - Keep the model loaded in memory10 * @param {boolean} keep - Keep the model loaded in memory
11 * @param {import('../users').UserDirectoryList} directories - The directories object for the user11 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
12 * @returns {Promise<number[][]>} - The array of vectors for the texts12 * @returns {Promise<number[][]>} - The array of vectors for the texts
13 */13 */
14async function getOllamaBatchVector(texts, apiUrl, model, keep, directories) {14export async function getOllamaBatchVector(texts, apiUrl, model, keep, directories) {
15 const result = [];15 const result = [];
16 for (const text of texts) {16 for (const text of texts) {
17 const vector = await getOllamaVector(text, apiUrl, model, keep, directories);17 const vector = await getOllamaVector(text, apiUrl, model, keep, directories);
@@ -26,10 +26,10 @@ async function getOllamaBatchVector(texts, apiUrl, model, keep, directories) {
26 * @param {string} apiUrl - The API URL26 * @param {string} apiUrl - The API URL
27 * @param {string} model - The model to use27 * @param {string} model - The model to use
28 * @param {boolean} keep - Keep the model loaded in memory28 * @param {boolean} keep - Keep the model loaded in memory
29 * @param {import('../users').UserDirectoryList} directories - The directories object for the user29 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
30 * @returns {Promise<number[]>} - The vector for the text30 * @returns {Promise<number[]>} - The vector for the text
31 */31 */
32async function getOllamaVector(text, apiUrl, model, keep, directories) {32export async function getOllamaVector(text, apiUrl, model, keep, directories) {
33 const url = new URL(apiUrl);33 const url = new URL(apiUrl);
34 url.pathname = '/api/embeddings';34 url.pathname = '/api/embeddings';
3535
@@ -54,6 +54,7 @@ async function getOllamaVector(text, apiUrl, model, keep, directories) {
54 throw new Error(`Ollama: Failed to get vector for text: ${response.statusText} ${responseText}`);54 throw new Error(`Ollama: Failed to get vector for text: ${response.statusText} ${responseText}`);
55 }55 }
5656
57 /** @type {any} */
57 const data = await response.json();58 const data = await response.json();
5859
59 if (!Array.isArray(data?.embedding)) {60 if (!Array.isArray(data?.embedding)) {
@@ -62,8 +63,3 @@ async function getOllamaVector(text, apiUrl, model, keep, directories) {
6263
63 return data.embedding;64 return data.embedding;
64}65}
65
66module.exports = {
67 getOllamaBatchVector,
68 getOllamaVector,
69};
src/vectors/openai-vectors.js+7 -11
@@ -1,5 +1,5 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { SECRET_KEYS, readSecret } = require('../endpoints/secrets');2import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';
33
4const SOURCES = {4const SOURCES = {
5 'togetherai': {5 'togetherai': {
@@ -23,11 +23,11 @@ const SOURCES = {
23 * Gets the vector for the given text batch from an OpenAI compatible endpoint.23 * Gets the vector for the given text batch from an OpenAI compatible endpoint.
24 * @param {string[]} texts - The array of texts to get the vector for24 * @param {string[]} texts - The array of texts to get the vector for
25 * @param {string} source - The source of the vector25 * @param {string} source - The source of the vector
26 * @param {import('../users').UserDirectoryList} directories - The directories object for the user26 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
27 * @param {string} model - The model to use for the embedding27 * @param {string} model - The model to use for the embedding
28 * @returns {Promise<number[][]>} - The array of vectors for the texts28 * @returns {Promise<number[][]>} - The array of vectors for the texts
29 */29 */
30async function getOpenAIBatchVector(texts, source, directories, model = '') {30export async function getOpenAIBatchVector(texts, source, directories, model = '') {
31 const config = SOURCES[source];31 const config = SOURCES[source];
3232
33 if (!config) {33 if (!config) {
@@ -61,6 +61,7 @@ async function getOpenAIBatchVector(texts, source, directories, model = '') {
61 throw new Error('API request failed');61 throw new Error('API request failed');
62 }62 }
6363
64 /** @type {any} */
64 const data = await response.json();65 const data = await response.json();
6566
66 if (!Array.isArray(data?.data)) {67 if (!Array.isArray(data?.data)) {
@@ -79,16 +80,11 @@ async function getOpenAIBatchVector(texts, source, directories, model = '') {
79 * Gets the vector for the given text from an OpenAI compatible endpoint.80 * Gets the vector for the given text from an OpenAI compatible endpoint.
80 * @param {string} text - The text to get the vector for81 * @param {string} text - The text to get the vector for
81 * @param {string} source - The source of the vector82 * @param {string} source - The source of the vector
82 * @param {import('../users').UserDirectoryList} directories - The directories object for the user83 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
83 * @param {string} model - The model to use for the embedding84 * @param {string} model - The model to use for the embedding
84 * @returns {Promise<number[]>} - The vector for the text85 * @returns {Promise<number[]>} - The vector for the text
85 */86 */
86async function getOpenAIVector(text, source, directories, model = '') {87export async function getOpenAIVector(text, source, directories, model = '') {
87 const vectors = await getOpenAIBatchVector([text], source, directories, model);88 const vectors = await getOpenAIBatchVector([text], source, directories, model);
88 return vectors[0];89 return vectors[0];
89}90}
90
91module.exports = {
92 getOpenAIVector,
93 getOpenAIBatchVector,
94};
src/vectors/vllm-vectors.js+8 -12
@@ -1,16 +1,16 @@
1const fetch = require('node-fetch').default;1import fetch from 'node-fetch';
2const { setAdditionalHeadersByType } = require('../additional-headers');2import { setAdditionalHeadersByType } from '../additional-headers.js';
3const { TEXTGEN_TYPES } = require('../constants');3import { TEXTGEN_TYPES } from '../constants.js';
44
5/**5/**
6 * Gets the vector for the given text from VLLM6 * Gets the vector for the given text from VLLM
7 * @param {string[]} texts - The array of texts to get the vectors for7 * @param {string[]} texts - The array of texts to get the vectors for
8 * @param {string} apiUrl - The API URL8 * @param {string} apiUrl - The API URL
9 * @param {string} model - The model to use9 * @param {string} model - The model to use
10 * @param {import('../users').UserDirectoryList} directories - The directories object for the user10 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
11 * @returns {Promise<number[][]>} - The array of vectors for the texts11 * @returns {Promise<number[][]>} - The array of vectors for the texts
12 */12 */
13async function getVllmBatchVector(texts, apiUrl, model, directories) {13export async function getVllmBatchVector(texts, apiUrl, model, directories) {
14 const url = new URL(apiUrl);14 const url = new URL(apiUrl);
15 url.pathname = '/v1/embeddings';15 url.pathname = '/v1/embeddings';
1616
@@ -31,6 +31,7 @@ async function getVllmBatchVector(texts, apiUrl, model, directories) {
31 throw new Error(`VLLM: Failed to get vector for text: ${response.statusText} ${responseText}`);31 throw new Error(`VLLM: Failed to get vector for text: ${response.statusText} ${responseText}`);
32 }32 }
3333
34 /** @type {any} */
34 const data = await response.json();35 const data = await response.json();
3536
36 if (!Array.isArray(data?.data)) {37 if (!Array.isArray(data?.data)) {
@@ -49,15 +50,10 @@ async function getVllmBatchVector(texts, apiUrl, model, directories) {
49 * @param {string} text - The text to get the vector for50 * @param {string} text - The text to get the vector for
50 * @param {string} apiUrl - The API URL51 * @param {string} apiUrl - The API URL
51 * @param {string} model - The model to use52 * @param {string} model - The model to use
52 * @param {import('../users').UserDirectoryList} directories - The directories object for the user53 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
53 * @returns {Promise<number[]>} - The vector for the text54 * @returns {Promise<number[]>} - The vector for the text
54 */55 */
55async function getVllmVector(text, apiUrl, model, directories) {56export async function getVllmVector(text, apiUrl, model, directories) {
56 const vectors = await getVllmBatchVector([text], apiUrl, model, directories);57 const vectors = await getVllmBatchVector([text], apiUrl, model, directories);
57 return vectors[0];58 return vectors[0];
58}59}
59
60module.exports = {
61 getVllmBatchVector,
62 getVllmVector,
63};