diff --git a/.gitignore b/.gitignore index b1d32a4..97b62d3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ logs !.env.example config.json + +pack-dist \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0dab4d2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/query-config"] + path = submodules/query-config + url = git@git.xiongxiao.me:kevisual/kevisual-query-config.git diff --git a/package.json b/package.json index dad5f05..727e9a2 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,18 @@ { - "name": "@kevisual/ai-center", + "name": "@kevisual/ai-center-services", "version": "0.0.1", "description": "", "main": "index.js", + "basename": "/root/ai-center-services", + "app": { + "entry": "dist/app.mjs", + "key": "ai-center-services", + "type": "system-app" + }, + "files": [ + "dist", + "types" + ], "scripts": { "build": "npm run clean && bun bun.config.mjs", "dev": "bun run --watch bun.config.mjs", @@ -29,9 +39,6 @@ "types": "./dist/ai-provider.d.ts" } }, - "files": [ - "dist" - ], "dependencies": {}, "devDependencies": { "@kevisual/code-center-module": "0.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caa7e63..5d43a74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,12 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 dotenv: specifier: ^16.5.0 version: 16.5.0 @@ -88,7 +94,7 @@ importers: version: 6.2.1(rollup@4.40.0)(typescript@5.8.3) sequelize: specifier: ^6.37.7 - version: 6.37.7(pg@8.14.1) + version: 6.37.7(pg-hstore@2.3.4)(pg@8.14.1) tape: specifier: ^5.9.0 version: 5.9.0 @@ -275,6 +281,9 @@ packages: '@kevisual/auth@1.0.5': resolution: {integrity: sha512-GwsLj7unKXi7lmMiIIgdig4LwwLiDJnOy15HHZR5gMbyK6s5/uJiMY5RXPB2+onGzTNDqFo/hXjsD2wkerHPVg==} + '@kevisual/cache@0.0.2': + resolution: {integrity: sha512-2Cl5KF2Gi27uLfhO6CdTMFnRzx9vYnqevAo7d9ab3rOaqTgF8tLeAXglXyRbaWW3WUbHU2XaOb4r98uUsqIQQw==} + '@kevisual/code-center-module@0.0.18': resolution: {integrity: sha512-BfANmxLEO1AwVmqpa6VDgxk//YN8asf1r5jIPpyKDQm12kyyrYgHND9AgGCDRH8lvq6rYVe0svCZXD5b06UPWQ==} peerDependencies: @@ -291,6 +300,15 @@ packages: '@kevisual/mark@0.0.7': resolution: {integrity: sha512-PiEEy4yvWEpixw76PzgrIWeNelzm+FrhtzFmqJU92o5GkgawaFwighcvIxqcVZRKeEFF4uvlTjFrGeQvXw6F4A==} + '@kevisual/permission@0.0.1': + resolution: {integrity: sha512-nSX2LzbPkU3YAMegbUFGU8tfmtFb7dcF5edqzm+gI6crcyCL1JzIB9HAYNEeEVIljLxuREwM/vVg9aFmF4cz9Q==} + + '@kevisual/query@0.0.13': + resolution: {integrity: sha512-gSEIDiCvwSaLLAFZv4vam4wSrMsaCuQ3VGjE3kwRwZ8urlVH1TOA+NUO908A22p9m1Iij7Y1Q/JlfSJi2QzuKQ==} + + '@kevisual/query@0.0.15': + resolution: {integrity: sha512-DK41qvyOiJMmlj70QyVP/48M0gszA39DdnBLtgU94YwAe6OqKrr9tYXHLjZrOROmUVMezIIBQuWMLedSAvb54A==} + '@kevisual/rollup-tools@0.0.1': resolution: {integrity: sha512-TdCN+IU0fyHudiiqYvobXQ8r5MltfM/cKmSS59iopyL8YYwXwcipOS4S24NWA79g7uwJfSUNk5lg3yVhom79fQ==} hasBin: true @@ -625,6 +643,9 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -760,6 +781,9 @@ packages: colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -771,6 +795,10 @@ packages: commander@2.15.1: resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -823,6 +851,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -917,6 +948,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} @@ -1137,6 +1171,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@11.0.1: resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==} engines: {node: 20 || >=22} @@ -1361,10 +1399,17 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.0: resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} engines: {node: 20 || >=22} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-git@0.7.8: resolution: {integrity: sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==} @@ -1381,11 +1426,6 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1398,9 +1438,15 @@ packages: lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.1.0: resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} engines: {node: 20 || >=22} @@ -1446,6 +1492,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1477,6 +1527,9 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1589,6 +1642,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} @@ -1610,6 +1667,10 @@ packages: pg-connection-string@2.7.0: resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + pg-hstore@2.3.4: + resolution: {integrity: sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==} + engines: {node: '>= 0.8.x'} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -1660,6 +1721,10 @@ packages: pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + pino-pretty@13.0.0: + resolution: {integrity: sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==} + hasBin: true + pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} @@ -1667,6 +1732,10 @@ packages: resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} hasBin: true + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + pm2-axon-rpc@0.7.1: resolution: {integrity: sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==} engines: {node: '>=5'} @@ -1694,6 +1763,24 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss@8.5.3: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} @@ -1741,6 +1828,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -1765,6 +1856,10 @@ packages: resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} engines: {node: '>=6'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1850,6 +1945,9 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -1988,6 +2086,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -2055,6 +2157,13 @@ packages: resolution: {integrity: sha512-czbGgxSVwRlbB3Ly/aqQrNwrDAzKHDW/kVXegp4hSFmR2c8qqm3hCgZbUy1+3QAQFGhPDG7J56UsV1uNilBFCA==} hasBin: true + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} @@ -2081,6 +2190,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.4.0: + resolution: {integrity: sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.19.3: resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} engines: {node: '>=18.0.0'} @@ -2389,7 +2517,7 @@ snapshots: ioredis: 5.6.1 nanoid: 5.1.5 pg: 8.14.1 - sequelize: 6.37.7(pg@8.14.1) + sequelize: 6.37.7(pg-hstore@2.3.4)(pg@8.14.1) socket.io: 4.8.1 zod: 3.24.2 transitivePeerDependencies: @@ -2410,7 +2538,7 @@ snapshots: cookie: 1.0.2 nanoid: 5.1.5 pg: 8.14.1 - sequelize: 6.37.7(pg@8.14.1) + sequelize: 6.37.7(pg-hstore@2.3.4)(pg@8.14.1) transitivePeerDependencies: - bufferutil - dotenv @@ -2427,6 +2555,24 @@ snapshots: - tedious - utf-8-validate + '@kevisual/permission@0.0.1': {} + + '@kevisual/query@0.0.13(ws@8.18.1)(zod@3.24.2)': + dependencies: + openai: 4.91.1(ws@8.18.1)(zod@3.24.2) + transitivePeerDependencies: + - encoding + - ws + - zod + + '@kevisual/query@0.0.15(ws@8.18.1)(zod@3.24.2)': + dependencies: + openai: 4.91.1(ws@8.18.1)(zod@3.24.2) + transitivePeerDependencies: + - encoding + - ws + - zod + '@kevisual/rollup-tools@0.0.1(esbuild@0.25.2)': dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.40.0) @@ -2765,6 +2911,8 @@ snapshots: ansi-styles@6.2.1: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2902,6 +3050,8 @@ snapshots: colorette@1.4.0: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -2910,6 +3060,8 @@ snapshots: commander@2.15.1: {} + commander@4.1.1: {} + commondir@1.0.1: {} concat-map@0.0.1: {} @@ -2959,6 +3111,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + dateformat@4.6.3: {} + dayjs@1.11.13: {} dayjs@1.8.36: {} @@ -3051,6 +3205,10 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + engine.io-parser@5.2.3: {} engine.io@6.6.4: @@ -3359,6 +3517,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@11.0.1: dependencies: foreground-child: 3.3.1 @@ -3613,10 +3780,18 @@ snapshots: isexe@2.0.0: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.0: dependencies: '@isaacs/cliui': 8.0.2 + joycon@3.1.1: {} + js-git@0.7.8: dependencies: bodec: 0.1.0 @@ -3636,8 +3811,6 @@ snapshots: json-stringify-safe@5.0.1: optional: true - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -3648,8 +3821,12 @@ snapshots: lodash.isarguments@3.1.0: {} + lodash.sortby@4.7.0: {} + lodash@4.17.21: {} + lru-cache@10.4.3: {} + lru-cache@11.1.0: {} lru-cache@6.0.0: @@ -3689,6 +3866,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + minimist@1.2.8: {} minipass@7.1.2: {} @@ -3717,6 +3898,12 @@ snapshots: mute-stream@0.0.8: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.11: {} nanoid@5.1.5: {} @@ -3818,6 +4005,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-scurry@2.0.0: dependencies: lru-cache: 11.1.0 @@ -3834,6 +4026,10 @@ snapshots: pg-connection-string@2.7.0: {} + pg-hstore@2.3.4: + dependencies: + underscore: 1.13.7 + pg-int8@1.0.1: {} pg-pool@3.8.0(pg@8.14.1): @@ -3883,6 +4079,22 @@ snapshots: dependencies: split2: 4.2.0 + pino-pretty@13.0.0: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pump: 3.0.2 + secure-json-parse: 2.7.0 + sonic-boom: 4.2.0 + strip-json-comments: 3.1.1 + pino-std-serializers@7.0.0: {} pino@9.6.0: @@ -3899,6 +4111,8 @@ snapshots: sonic-boom: 4.2.0 thread-stream: 3.1.0 + pirates@4.0.7: {} + pm2-axon-rpc@0.7.1: dependencies: debug: 4.4.0 @@ -3974,6 +4188,13 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-load-config@6.0.1(postcss@8.5.3)(tsx@4.19.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.3 + tsx: 4.19.3 + postcss@8.5.3: dependencies: nanoid: 3.3.11 @@ -4023,6 +4244,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + real-require@0.2.0: {} redis-errors@1.2.0: {} @@ -4059,6 +4282,8 @@ snapshots: transitivePeerDependencies: - supports-color + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.10: @@ -4178,6 +4403,8 @@ snapshots: sax@1.4.1: {} + secure-json-parse@2.7.0: {} + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.11 @@ -4191,7 +4418,7 @@ snapshots: sequelize-pool@7.1.0: {} - sequelize@6.37.7(pg@8.14.1): + sequelize@6.37.7(pg-hstore@2.3.4)(pg@8.14.1): dependencies: '@types/debug': 4.1.12 '@types/validator': 13.12.3 @@ -4211,6 +4438,7 @@ snapshots: wkx: 0.5.0 optionalDependencies: pg: 8.14.1 + pg-hstore: 2.3.4 transitivePeerDependencies: - supports-color @@ -4336,6 +4564,10 @@ snapshots: source-map@0.6.1: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + sourcemap-codec@1.4.8: {} split2@4.2.0: {} @@ -4428,6 +4660,14 @@ snapshots: resolve: 2.0.0-next.5 string.prototype.trim: 1.2.10 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thread-stream@3.1.0: dependencies: real-require: 0.2.0 @@ -4451,6 +4691,33 @@ snapshots: tslib@2.8.1: {} + tsup@8.4.0(postcss@8.5.3)(tsx@4.19.3)(typescript@5.8.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.0(supports-color@5.5.0) + esbuild: 0.25.2 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.3)(tsx@4.19.3) + resolve-from: 5.0.0 + rollup: 4.39.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.12 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.3 + typescript: 5.8.2 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.19.3: dependencies: esbuild: 0.25.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..cbe79b2 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'submodules/**' diff --git a/src/demo-route.ts b/src/demo-route.ts index aabbc93..6ea3731 100644 --- a/src/demo-route.ts +++ b/src/demo-route.ts @@ -1,5 +1,21 @@ import { app } from './app.ts'; import { useConfig } from '@kevisual/use-config/env'; +const config = useConfig(); + +app + .route({ + path: 'auth', + key: 'auth', + id: 'auth', + }) + .define(async (ctx) => { + ctx.state.tokenUser = { + id: 'abcdefff', + username: 'root', + }; + ctx.query.token = config.ROOT_TEST_TOKEN; + }) + .addTo(app); app .route({ @@ -11,6 +27,4 @@ app }) .addTo(app); -const config = useConfig(); - console.log('run demo: http://localhost:' + config.PORT + '/api/router?path=demo&key=demo'); diff --git a/src/dev.ts b/src/dev.ts index d089c88..ef7737d 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -1,10 +1,7 @@ import { useConfig } from '@kevisual/use-config/env'; -import { useContextKey } from '@kevisual/use-config/context'; -import { Redis } from 'ioredis'; -export const redis = useContextKey('redis', () => { - return new Redis(); -}); -import { app } from './index.ts'; // 开发环境 +import { app } from './app.ts'; +import './demo-route.ts'; +import './routes/index.ts'; const config = useConfig(); diff --git a/src/index.ts b/src/index.ts index d7c7c49..f2dc5d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ import { app } from './app.ts'; -import './demo-route.ts'; +import './routes/index.ts'; -export { app }; \ No newline at end of file +export { app }; diff --git a/src/logger/index.ts b/src/logger/index.ts new file mode 100644 index 0000000..1f6fe32 --- /dev/null +++ b/src/logger/index.ts @@ -0,0 +1,37 @@ +import { pino } from 'pino'; +import { useConfig } from '@kevisual/use-config/env'; + +const config = useConfig(); + +export const logger = pino({ + level: config.LOG_LEVEL || 'info', + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + }, + serializers: { + error: pino.stdSerializers.err, + req: pino.stdSerializers.req, + res: pino.stdSerializers.res, + }, + base: { + app: 'ai-chat', + env: process.env.NODE_ENV || 'development', + }, +}); + +export const logError = (message: string, data?: any) => logger.error({ data }, message); +export const logWarning = (message: string, data?: any) => logger.warn({ data }, message); +export const logInfo = (message: string, data?: any) => logger.info({ data }, message); +export const logDebug = (message: string, data?: any) => logger.debug({ data }, message); + +export const log = { + error: logError, + warn: logWarning, + info: logInfo, + debug: logDebug, +}; diff --git a/src/modules/db.ts b/src/modules/db.ts index 0ec1fb5..64381ea 100644 --- a/src/modules/db.ts +++ b/src/modules/db.ts @@ -1,3 +1,16 @@ import { useContextKey } from '@kevisual/use-config/context'; +import { Redis } from 'ioredis'; +export const redis = useContextKey('redis', () => { + const redis = new Redis({ + host: 'localhost', + port: 6379, + }); + return redis; +}); -export const redis = useContextKey('redis'); +const checkConnection = async () => { + const res = await redis.ping(); + console.log('redis ping', res); +}; + +// checkConnection(); diff --git a/src/modules/query.ts b/src/modules/query.ts new file mode 100644 index 0000000..23bba1f --- /dev/null +++ b/src/modules/query.ts @@ -0,0 +1,12 @@ +import { Query } from '@kevisual/query/query'; +import { QueryConfig } from '@kevisual/query-config'; +import { config } from './config.ts'; + +const baseURL = new URL(config.path, config.host); +export const query = new Query({ + url: baseURL.toString(), +}); + +export const queryConfig = new QueryConfig({ + query: query as any, +}); diff --git a/src/modules/sequelize.ts b/src/modules/sequelize.ts new file mode 100644 index 0000000..38d8f0d --- /dev/null +++ b/src/modules/sequelize.ts @@ -0,0 +1,30 @@ +import { Sequelize } from 'sequelize'; +import { useConfig } from '@kevisual/use-config/env'; +export const config = useConfig() as any; + +export type PostgresConfig = { + postgres: { + username: string; + password: string; + host: string; + port: number; + database: string; + }; +}; +if (!config.POSTGRES_PASSWORD || !config.POSTGRES_USER) { + console.error('postgres config is required password and user'); + process.exit(1); +} +const postgresConfig = { + username: config.POSTGRES_USER, + password: config.POSTGRES_PASSWORD, + host: config.POSTGRES_HOST || 'localhost', + port: parseInt(config.POSTGRES_PORT || '5432'), + database: config.POSTGRES_DB || 'postgres', +}; +// connect to db +export const sequelize = new Sequelize({ + dialect: 'postgres', + ...postgresConfig, + // logging: false, +}); diff --git a/src/provider/chat-adapter/custom.ts b/src/provider/chat-adapter/custom.ts index ffae8df..1d29255 100644 --- a/src/provider/chat-adapter/custom.ts +++ b/src/provider/chat-adapter/custom.ts @@ -7,6 +7,7 @@ export type OllamaOptions = BaseChatOptions; */ export class Custom extends BaseChat { constructor(options: OllamaOptions) { - super(options); + const baseURL = options.baseURL || 'https://api.deepseek.com/v1/'; + super({ ...(options as BaseChatOptions), baseURL: baseURL }); } } diff --git a/src/provider/chat-adapter/deepseek.ts b/src/provider/chat-adapter/deepseek.ts index 4508708..6313567 100644 --- a/src/provider/chat-adapter/deepseek.ts +++ b/src/provider/chat-adapter/deepseek.ts @@ -3,6 +3,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts'; export type DeepSeekOptions = Partial; export class DeepSeek extends BaseChat { constructor(options: DeepSeekOptions) { - super({ baseURL: 'https://api.deepseek.com/v1/', ...options } as any); + const baseURL = options.baseURL || 'https://api.deepseek.com/v1/'; + super({ ...(options as BaseChatOptions), baseURL: baseURL }); } } diff --git a/src/provider/chat-adapter/model-scope.ts b/src/provider/chat-adapter/model-scope.ts index 4bb8b04..2980a23 100644 --- a/src/provider/chat-adapter/model-scope.ts +++ b/src/provider/chat-adapter/model-scope.ts @@ -4,6 +4,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts'; export type ModelScopeOptions = Partial; export class ModelScope extends BaseChat { constructor(options: ModelScopeOptions) { - super({ baseURL: 'https://api-inference.modelscope.cn/v1/', ...options } as any); + const baseURL = options.baseURL || 'https://api-inference.modelscope.cn/v1/'; + super({ ...options, baseURL: baseURL } as any); } } diff --git a/src/provider/chat-adapter/ollama.ts b/src/provider/chat-adapter/ollama.ts index bb4e792..66733d2 100644 --- a/src/provider/chat-adapter/ollama.ts +++ b/src/provider/chat-adapter/ollama.ts @@ -21,7 +21,8 @@ type OllamaModel = { }; export class Ollama extends BaseChat { constructor(options: OllamaOptions) { - super({ baseURL: 'http://localhost:11434/v1', ...(options as BaseChatOptions) }); + const baseURL = options.baseURL || 'http://localhost:11434/v1'; + super({ ...(options as BaseChatOptions), baseURL: baseURL }); } async chat(messages: ChatMessage[], options?: ChatMessageOptions) { const res = await super.chat(messages, options); diff --git a/src/provider/chat-adapter/siliconflow.ts b/src/provider/chat-adapter/siliconflow.ts index 7948e5f..5685e34 100644 --- a/src/provider/chat-adapter/siliconflow.ts +++ b/src/provider/chat-adapter/siliconflow.ts @@ -25,7 +25,8 @@ type SiliconFlowUsageResponse = { }; export class SiliconFlow extends BaseChat { constructor(options: SiliconFlowOptions) { - super({ baseURL: 'https://api.siliconflow.com/v1', ...(options as BaseChatOptions) }); + const baseURL = options.baseURL || 'https://api.siliconflow.com/v1'; + super({ ...(options as BaseChatOptions), baseURL: baseURL }); } async getUsageInfo(): Promise { return this.openai.get('/user/info'); diff --git a/src/provider/chat-adapter/volces.ts b/src/provider/chat-adapter/volces.ts index 7ffe707..ba12fc7 100644 --- a/src/provider/chat-adapter/volces.ts +++ b/src/provider/chat-adapter/volces.ts @@ -3,6 +3,7 @@ import { BaseChat, BaseChatOptions } from '../core/chat.ts'; export type VolcesOptions = Partial; export class Volces extends BaseChat { constructor(options: VolcesOptions) { - super({ baseURL: 'https://ark.cn-beijing.volces.com/api/v3/', ...options } as any); + const baseURL = options.baseURL || 'https://ark.cn-beijing.volces.com/api/v3/'; + super({ ...(options as BaseChatOptions), baseURL: baseURL }); } } diff --git a/src/provider/core/type.ts b/src/provider/core/type.ts index 1edfc02..01db2d6 100644 --- a/src/provider/core/type.ts +++ b/src/provider/core/type.ts @@ -1,9 +1,6 @@ import OpenAI from 'openai'; -import { APIPromise } from 'openai/core.mjs'; -import { ChatCompletionChunk } from 'openai/resources.mjs'; -import { Stream } from 'openai/streaming.mjs'; -export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam; +export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam ; export type ChatMessageOptions = Partial; export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion; export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion; diff --git a/src/provider/index.ts b/src/provider/index.ts index c21bb04..d77c050 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -40,6 +40,8 @@ export class ProviderManager { if (!Provider) { throw new Error(`Provider ${provider} not found`); } + console.log('pm', 'Provider', ProviderMap[provider]); + this.provider = new Provider({ model, apiKey, diff --git a/src/provider/utils/ai-config-type.ts b/src/provider/utils/ai-config-type.ts new file mode 100644 index 0000000..a4ed0c6 --- /dev/null +++ b/src/provider/utils/ai-config-type.ts @@ -0,0 +1,52 @@ +import type { Permission } from '@kevisual/permission'; + +export type AIModel = { + /** + * 提供商 + */ + provider: string; + /** + * 模型名称 + */ + model: string; + /** + * 模型组 + */ + group: string; + /** + * 每日请求频率限制 + */ + dayLimit?: number; + /** + * 总的token限制 + */ + tokenLimit?: number; +}; +export type SecretKey = { + /** + * 组 + */ + group: string; + /** + * API密钥 + */ + apiKey: string; + /** + * 解密密钥 + */ + decryptKey?: string; +}; + +export type AIConfig = { + title?: string; + description?: string; + models: AIModel[]; + secretKeys: SecretKey[]; + permission?: Permission; + filter?: { + objectKey: string; + type: 'array' | 'object'; + operate: 'removeAttribute' | 'remove'; + attribute: string[]; + }[]; +}; diff --git a/src/provider/utils/parse-config.ts b/src/provider/utils/parse-config.ts index d74bc1a..05859cf 100644 --- a/src/provider/utils/parse-config.ts +++ b/src/provider/utils/parse-config.ts @@ -1,14 +1,15 @@ -import { AES, enc } from 'crypto-js'; +import { Permission } from '@kevisual/permission'; +import CryptoJS from 'crypto-js'; // 加密函数 export function encryptAES(plainText: string, secretKey: string) { - return AES.encrypt(plainText, secretKey).toString(); + return CryptoJS.AES.encrypt(plainText, secretKey).toString(); } // 解密函数 export function decryptAES(cipherText: string, secretKey: string) { - const bytes = AES.decrypt(cipherText, secretKey); - return bytes.toString(enc.Utf8); + const bytes = CryptoJS.AES.decrypt(cipherText, secretKey); + return bytes.toString(CryptoJS.enc.Utf8); } type AIModel = { @@ -25,9 +26,13 @@ type AIModel = { */ group: string; /** - * 每日限制 + * 每日请求频率限制 */ dayLimit?: number; + /** + * 总的token限制 + */ + tokenLimit?: number; }; type SecretKey = { @@ -56,6 +61,7 @@ export type ProviderResult = { group: string; apiKey: string; dayLimit?: number; + tokenLimit?: number; baseURL?: string; /** * 解密密钥 @@ -68,6 +74,13 @@ export type AIConfig = { description?: string; models: AIModel[]; secretKeys: SecretKey[]; + permission?: Permission; + filter?: { + objectKey: string; + type: 'array' | 'object'; + operate: 'removeAttribute' | 'remove'; + attribute: string[]; + }[]; }; export class AIConfigParser { private config: AIConfig; @@ -75,7 +88,11 @@ export class AIConfigParser { constructor(config: AIConfig) { this.config = config; } - + /** + * 获取模型配置 + * @param opts + * @returns + */ getProvider(opts: GetProviderOpts): ProviderResult { const { model, group, decryptKey } = opts; const modelConfig = this.config.models.find((m) => m.model === model && m.group === group); @@ -104,16 +121,17 @@ export class AIConfigParser { this.result = mergeConfig; return mergeConfig; } - - async getSecretKey({ - getCache, - setCache, - providerResult, - }: { + /** + * 获取解密密钥 + * @param opts + * @returns + */ + async getSecretKey(opts?: { getCache?: (key: string) => Promise; setCache?: (key: string, value: string) => Promise; providerResult?: ProviderResult; }) { + const { getCache, setCache, providerResult } = opts || {}; const { apiKey, decryptKey, group = '', model } = providerResult || this.result; const cacheKey = `${group}--${model}`; if (!decryptKey) { @@ -131,11 +149,38 @@ export class AIConfigParser { } return secretKey; } + /** + * 加密 + * @param plainText + * @param secretKey + * @returns + */ encrypt(plainText: string, secretKey: string) { return encryptAES(plainText, secretKey); } - + /** + * 解密 + * @param cipherText + * @param secretKey + * @returns + */ decrypt(cipherText: string, secretKey: string) { return decryptAES(cipherText, secretKey); } + + /** + * 获取模型配置 + * @returns + */ + getSelectOpts() { + const { models, secretKeys = [] } = this.config; + + return models.map((model) => { + const selectOpts = secretKeys.find((m) => m.group === model.group); + return { + ...model, + ...selectOpts, + }; + }); + } } diff --git a/src/routes/ai-chat/cache.ts b/src/routes/ai-chat/cache.ts new file mode 100644 index 0000000..80887a7 --- /dev/null +++ b/src/routes/ai-chat/cache.ts @@ -0,0 +1,43 @@ +import { app } from '@/app.ts'; +import { ChatConfigServices } from './services/chat-config-srevices.ts'; +import { log } from '@/logger/index.ts'; +import { ChatServices } from './services/chat-services.ts'; + +/** + * 清除缓存 + */ +// https://localhost:4000/api/router?path=ai&key=clear-cache +app + .route({ + path: 'ai', + key: 'clear-cache', + description: '清除缓存', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const username = tokenUser.username; + const services = new ChatConfigServices(username, username); + await services.clearCache(); + log.info('清除缓存成功', { username }); + ctx.body = 'success'; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'clear-chat-limit', + description: '清除chat使用情况', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const username = tokenUser.username; + const cache = await ChatServices.clearChatLimit(username); + log.debug('清除chat使用情况成功', { username, cache }); + ctx.body = { + cache, + }; + }) + .addTo(app); diff --git a/src/routes/ai-chat/index.ts b/src/routes/ai-chat/index.ts index 2c891c2..42034bc 100644 --- a/src/routes/ai-chat/index.ts +++ b/src/routes/ai-chat/index.ts @@ -1,11 +1,249 @@ import { app } from '@/app.ts'; - +import { ChatServices } from './services/chat-services.ts'; +import { ChatConfigServices } from './services/chat-config-srevices.ts'; +import { AiChatHistoryModel } from './models/ai-chat-history.ts'; +import { UserPermission } from '@kevisual/permission'; +import { AIConfigParser } from '@/provider/utils/parse-config.ts'; +import { log } from '@/logger/index.ts'; app .route({ path: 'ai', key: 'chat', + middleware: ['auth'], }) - .define(async () => { - // + .define(async (ctx) => { + const data = ctx.query.data || {}; + const { id, messages = [], title, type } = data; + const hook = data.data?.hook; + + let { username, model, group, getFull = false } = ctx.query; + const tokenUser = ctx.state.tokenUser || {}; + const tokenUsername = tokenUser.username; + const options = ctx.query.options || {}; + let aiChatHistory: AiChatHistoryModel; + if (id) { + aiChatHistory = await AiChatHistoryModel.findByPk(id); + if (!aiChatHistory) { + ctx.throw(400, 'aiChatHistory not found'); + } + if (aiChatHistory.uid !== tokenUser.id) { + ctx.throw(403, 'not permission'); + } + username = username || aiChatHistory.username || tokenUsername; + model = model || aiChatHistory.model; + group = group || aiChatHistory.group; + } else { + username = username || tokenUsername; + } + const isSelf = username === tokenUsername; + + if (!Array.isArray(messages)) { + ctx.throw(400, 'chat messages is not array'); + } + // 初始化服务 + const chatServices = await ChatServices.createServices({ + owner: username, + model, + group, + username: tokenUsername, + }); + if (!isSelf && username !== 'root') { + const aiConfig = chatServices.aiConfig; + const permission = new UserPermission({ permission: aiConfig.permission, owner: username }); + const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: options.password }); + if (!checkPermission.success) { + ctx.throw(403, checkPermission.message); + } + } + + const chatConfigServices = new ChatConfigServices(username, tokenUsername); + await chatConfigServices.checkUserCanChat(tokenUsername); + await chatServices.checkCanChat(); + const pickMessages = await chatServices.chatMessagePick(messages); + if (pickMessages.length === 0) { + ctx.throw(400, 'chat messages is empty'); + } + if (!aiChatHistory) { + aiChatHistory = await AiChatHistoryModel.create({ + username, + model, + group, + title, + type: type || 'keep', + }); + if (!title) { + // TODO: 创建标题 + } + } + let message; + try { + const res = await chatServices.chat(pickMessages, options); + message = res.choices[0].message; + } catch (error) { + log.error('chat error', { + errorMessage: error.message, + }); + ctx.throw(500, error.message); + } + try { + const newMessage = await chatServices.createNewMessage([...messages, message]); + + const usage = chatServices.chatProvider.getChatUsage(); + await chatServices.updateChatLimit(usage.total_tokens); + await chatConfigServices.updateUserChatLimit(tokenUsername, usage.total_tokens); + + const needUpdateData: any = { + messages: newMessage, + prompt_tokens: aiChatHistory.prompt_tokens + usage.prompt_tokens, + completion_tokens: aiChatHistory.completion_tokens + usage.completion_tokens, + total_tokens: aiChatHistory.total_tokens + usage.total_tokens, + version: aiChatHistory.version + 1, + model: model, + group: group, + username: username, + }; + if (hook) { + needUpdateData.data = { + ...aiChatHistory.data, + hook, + }; + } + if (type) { + needUpdateData.type = type; + } + await AiChatHistoryModel.update(needUpdateData, { where: { id: aiChatHistory.id } }); + ctx.body = { + message: newMessage[newMessage.length - 1], + updatedAt: aiChatHistory.updatedAt, + version: aiChatHistory.version, + aiChatHistory: getFull || !id ? aiChatHistory : undefined, + }; + } catch (error) { + console.error('create new message error', error); + ctx.throw(500, error.message); + } + }) + .addTo(app); + +// http://localhost:4010/api/router?path=ai&key=question&question="1 and 1 equals" +app + .route({ + path: 'ai', + key: 'question', + middleware: ['auth'], + isDebug: true, + }) + .define(async (ctx) => { + const data = ctx.query; + const model = data.model || 'qwq:latest'; + const group = data.group || 'ollama'; + const tokenUser = ctx.state.tokenUser; + const chatServices = await ChatServices.createServices({ + owner: data.username || 'root', + model, + group, + username: tokenUser.username, + }); + const res = await chatServices.chat([ + { + role: 'user', + content: data.question, + }, + ]); + ctx.body = res; + }) + .addTo(app); + +// http://localhost:4010/api/router?path=ai&key=get-model-list +app + .route({ + path: 'ai', + key: 'get-model-list', + middleware: ['auth'], + description: '获取模型列表', + isDebug: true, + }) + .define(async (ctx) => { + const username = ctx.query.username || 'root'; + const tokenUser = ctx.state.tokenUser; + const usernames = ctx.query.data?.usernames || []; + const keepSecret = ctx.query.keepSecret || false; + const tokenUsername = tokenUser.username; + const isSameUser = username === tokenUser.username; + const configArray: any[] = []; + try { + const services = new ChatConfigServices(username, tokenUser.username); + const res = await services.getChatConfig(services.isOwner && keepSecret, ctx.query.token); + const selectOpts = await services.getSelectOpts(res); + configArray.push({ + username, + config: res, + selectOpts, + self: isSameUser, + }); + if (!isSameUser) { + const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username); + const selfRes = await selfServices.getChatConfig(services.isOwner && keepSecret, ctx.query.token); + const selfSelectOpts = await selfServices.getSelectOpts(selfRes); + configArray.push({ + username: tokenUser.username, + self: true, + config: selfRes, + selectOpts: selfSelectOpts, + }); + } + for (const username of usernames) { + const services = new ChatConfigServices(username, tokenUser.username); + const res = await services.getChatConfig(services.isOwner && keepSecret, ctx.query.token); + const aiConfig = services.aiConfig; + const permission = new UserPermission({ permission: aiConfig.permission, owner: username }); + const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: '-----------------' }); + if (!checkPermission.success) { + configArray.push({ + username, + config: null, + error: checkPermission.message, + }); + } else { + const selectOpts = await services.getSelectOpts(res); + configArray.push({ + username, + config: res, + selectOpts, + self: username === tokenUser.username, + }); + } + } + ctx.body = { list: configArray }; + } catch (error) { + log.error('get model list error', { + username, + errorMessage: error.message, + errorStack: error.stack, + }); + ctx.throw(500, error.message); + } + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'get-chat-usage', + description: '获取chat使用情况, 只获取root的使用情况', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser || {}; + const username = tokenUser.username; + const services = new ChatConfigServices('root', username); + const chatServices = await ChatServices.createServices({ owner: username, username }); + const rootUsage = await services.getUserChatLimit(username); + const selfUsage = await chatServices.getChatLimit(); + + ctx.body = { + rootUsage, + selfUsage, + }; }) .addTo(app); diff --git a/src/routes/ai-chat/list.ts b/src/routes/ai-chat/list.ts new file mode 100644 index 0000000..07eaea4 --- /dev/null +++ b/src/routes/ai-chat/list.ts @@ -0,0 +1,139 @@ +import { app } from '@/app.ts'; +import { AiChatHistoryModel } from './models/ai-chat-history.ts'; + +app + .route({ + path: 'ai', + key: 'get-chat-list', + description: '获取chat列表', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const type = ctx.query.type || 'keep'; + const all = ctx.query.all || false; + let search: any = {}; + if (type && !all) { + search.type = type; + } + const aiChatList = await AiChatHistoryModel.findAll({ + where: { + uid: tokenUser.id, + ...search, + }, + order: [['updatedAt', 'DESC']], + }); + ctx.body = { + list: aiChatList, + }; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'update-chat', + description: '更新chat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const uid = tokenUser.id; + const { id, data, prompt_tokens, total_tokens, completion_tokens, createdAt, updatedAt, ...rest } = ctx.query.data || {}; + let aiChat: AiChatHistoryModel | null = null; + if (id) { + aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== uid) { + ctx.throw(403, 'no permission'); + } + await aiChat.update({ data: { ...aiChat.data, ...data }, ...rest, version: aiChat.version + 1 }); + } else { + aiChat = await AiChatHistoryModel.create({ + ...rest, + uid: uid, + }); + } + ctx.body = aiChat; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'get-chat', + description: '获取chat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query.data || {}; + if (!id) { + ctx.throw(400, 'id is required'); + } + const aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = aiChat; + }) + .addTo(app); +app + .route({ + path: 'ai', + key: 'get-chat-version', + description: '获取chat版本', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query.data || {}; + if (!id) { + ctx.throw(400, 'id is required'); + } + const aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = { + id: aiChat.id, + version: aiChat.version, + }; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'delete-chat', + description: '删除chat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query.data || {}; + if (!id) { + ctx.throw(400, 'id is required'); + } + const aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + await aiChat.destroy(); + + ctx.body = { + success: true, + }; + }) + .addTo(app); diff --git a/src/routes/ai-chat/models/ai-chat-history.ts b/src/routes/ai-chat/models/ai-chat-history.ts new file mode 100644 index 0000000..762ba59 --- /dev/null +++ b/src/routes/ai-chat/models/ai-chat-history.ts @@ -0,0 +1,116 @@ +import { sequelize } from '@/modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; + +export type AiChatHistory = Partial>; + +export type ChastHistoryMessage = { + role: string; + content: string; + name: string; + id?: string; + createdAt?: number; + updatedAt?: number; + hide?: boolean; + noUse?: boolean; +}; +type AiChatHistoryData = { + hook?: { + [key: string]: any; + }; +}; +export const aiChatTypes = ['keep', 'temp', 'archive'] as const; +export type AIChatType = (typeof aiChatTypes)[number]; +export class AiChatHistoryModel extends Model { + declare id: string; + declare username: string; + declare model: string; + declare group: string; + + declare title: string; + + declare messages: ChastHistoryMessage[]; + declare uid: string; + declare data: AiChatHistoryData; + declare prompt_tokens: number; + declare total_tokens: number; + declare completion_tokens: number; + + declare version: number; + declare type: string; + + declare createdAt: Date; + declare updatedAt: Date; +} + +AiChatHistoryModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: '', + }, + model: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: '', + }, + group: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: '', + }, + title: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: '', + }, + messages: { + type: DataTypes.JSONB, + allowNull: false, + defaultValue: [], + }, + prompt_tokens: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + total_tokens: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + completion_tokens: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + data: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + type: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'keep', // keep 保留 temp 临时 + }, + version: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + uid: { + type: DataTypes.UUID, + allowNull: true, + }, + }, + { + sequelize, + tableName: 'kv_ai_chat_history', + paranoid: false, + }, +); + +AiChatHistoryModel.sync({ alter: true, logging: false }).catch((e) => { + console.error('AiChatHistoryModel sync', e); +}); diff --git a/src/routes/ai-chat/services/chat-config-srevices.ts b/src/routes/ai-chat/services/chat-config-srevices.ts new file mode 100644 index 0000000..3e7e51d --- /dev/null +++ b/src/routes/ai-chat/services/chat-config-srevices.ts @@ -0,0 +1,152 @@ +import { AIConfigParser, type AIConfig } from '@/provider/utils/parse-config.ts'; +import { redis } from '@/modules/db.ts'; +import { CustomError } from '@kevisual/router'; +import { queryConfig } from '@/modules/query.ts'; +import { log } from '@/logger/index.ts'; +export class ChatConfigServices { + cachePrefix = 'ai:chat:config'; + // 使用谁的模型 + owner: string; + // 使用者 + username: string; + aiConfig?: AIConfig; + isOwner: boolean; + /** + * username 是使用的模型的用户名,使用谁的模型 + * @param username + */ + constructor(owner: string, username: string, token?: string) { + this.owner = owner; + this.username = username; + this.isOwner = owner === username; + } + getKey() { + return `${this.cachePrefix}:${this.owner}`; + } + /** + * 获取chat配置 + * @param keepSecret 是否需要清除secret 默认 不清除 为true + * @returns + */ + async getChatConfig(keepSecret = true, token?: string) { + const key = this.getKey(); + const cache = await redis.get(key); + let modelConfig = null; + if (cache) { + modelConfig = JSON.parse(cache); + } + if (!modelConfig) { + if (this.owner !== this.username) { + throw new CustomError( + `the owner [${this.owner}] config, [${this.username}] not permission to init config, only owner can init config, place connect owner`, + ); + } else { + const res = await queryConfig.getConfigByKey('ai.json', { token }); + if (res.code === 200 && res.data?.data) { + modelConfig = res.data.data; + } else { + throw new CustomError(400, 'get config failed'); + } + } + } + if (!modelConfig) { + throw new CustomError(`${this.owner} modelConfig is null`); + } + if (!cache) { + const cacheTime = 60 * 60 * 24 * 40; // 1天 + await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime); + } + this.aiConfig = modelConfig; + if (!keepSecret) { + modelConfig = this.filterApiKey(modelConfig); + } + return modelConfig; + } + async clearCache() { + const key = this.getKey(); + await redis.set(key, JSON.stringify({}), 'EX', 1); + } + /** + * 获取模型配置 + * @returns + */ + async getSelectOpts(config?: AIConfig) { + const aiConfigParser = new AIConfigParser(config || this.aiConfig); + return aiConfigParser.getSelectOpts(); + } + async filterApiKey(chatConfig: AIConfig) { + // 过滤掉secret中的所有apiKey,移除掉并返回chatConfig + const { secretKeys = [], ...rest } = chatConfig; + return { + ...rest, + secretKeys: secretKeys.map((item) => { + return { + ...item, + apiKey: undefined, + decryptKey: undefined, + }; + }), + }; + } + /** + * 获取和检测当前用户的额度, 当使用 root 账号的时候,才需要检测 + * username是当前使用用户 + * @param username + */ + async checkUserCanChat(username: string) { + if (this.owner !== 'root') return true; + const maxToken = 100000; + const userCacheKey = `${this.cachePrefix}:root:chat-limit:${username}`; + const cache = await redis.get(userCacheKey); + if (cache) { + const cacheData = JSON.parse(cache); + if (cacheData.token >= maxToken) { + throw new CustomError(400, 'use root account token limit exceeded'); + } + } + return true; + } + /** + * 获取用户的使用情况 + * username是当前使用用户 + * @param username + * @returns + */ + async getUserChatLimit(username: string) { + if (this.owner !== 'root') return; + const userCacheKey = `${this.cachePrefix}:root:chat-limit:${username}`; + const cache = await redis.get(userCacheKey); + if (cache) { + const cacheData = JSON.parse(cache); + return cacheData; + } + return { + token: 0, + day: 0, + }; + } + + /** + * 更新用户的使用情况 + * username是当前使用用户 + * @param username + * @param token + */ + async updateUserChatLimit(username: string, token: number) { + if (this.owner !== 'root') return; + const userCacheKey = `${this.cachePrefix}:root:chat-limit:${username}`; + const cache = await redis.get(userCacheKey); + if (cache) { + const cacheData = JSON.parse(cache); + cacheData.token = cacheData.token + token; + await redis.set(userCacheKey, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天 + } else { + await redis.set(userCacheKey, JSON.stringify({ token }), 'EX', 60 * 60 * 24 * 30); // 30天 + } + } + async clearChatLimit() { + if (this.owner !== 'root') return; + // const userCacheKey = `${this.cachePrefix}:root:chat-limit:${this.username}`; + // await redis.del(userCacheKey); + } +} diff --git a/src/routes/ai-chat/services/chat-services.ts b/src/routes/ai-chat/services/chat-services.ts index d98292b..f02a75a 100644 --- a/src/routes/ai-chat/services/chat-services.ts +++ b/src/routes/ai-chat/services/chat-services.ts @@ -1,9 +1,16 @@ -import { AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts'; -import { ProviderManager, ChatMessage, BaseChat } from '@/provider/index.ts'; -import { getChatConfig } from '@/modules/chat-config.ts'; +import { AIConfig, AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts'; +import { ProviderManager, ChatMessage, BaseChat, ChatMessageOptions } from '@/provider/index.ts'; import { redis } from '@/modules/db.ts'; +import { CustomError } from '@kevisual/router'; +import { ChatConfigServices } from './chat-config-srevices.ts'; +import { pick } from 'lodash-es'; +import { ChastHistoryMessage } from '../models/ai-chat-history.ts'; +import { nanoid } from '@/utils/uuid.ts'; +import dayjs from 'dayjs'; +import { log } from '@/logger/index.ts'; + export type ChatServicesConfig = { - username: string; + owner: string; model: string; group: string; decryptKey?: string; @@ -13,7 +20,7 @@ export class ChatServices { /** * 用户名 */ - username: string; + owner: string; /** * 模型 */ @@ -30,9 +37,10 @@ export class ChatServices { * 模型配置 */ modelConfig?: ProviderResult; + aiConfig?: AIConfig; chatProvider?: BaseChat; constructor(opts: ChatServicesConfig) { - this.username = opts.username; + this.owner = opts.owner; this.model = opts.model; this.group = opts.group; this.decryptKey = opts.decryptKey; @@ -41,8 +49,8 @@ export class ChatServices { * 初始化 * @returns */ - async init() { - const config = await this.getConfig(); + async init(username: string) { + const config = await this.getConfig(username); const aiConfigParser = new AIConfigParser(config); const model = this.model; const group = this.group; @@ -52,40 +60,189 @@ export class ChatServices { const apiKey = await aiConfigParser.getSecretKey({ getCache: async (key) => { const cache = await redis.get(that.wrapperKey(key)); - return cache; + return cache ? JSON.parse(cache) : null; }, setCache: async (key, value) => { - await redis.set(that.wrapperKey(key), value); + await redis.set(that.wrapperKey(key), JSON.stringify(value), 'EX', 60 * 60 * 24 * 1); // 1天 }, }); that.modelConfig = { ...providerResult, apiKey }; + that.aiConfig = config; return that.modelConfig; } - async wrapperKey(key: string) { - const username = this.username; - return `${this.cachePrefix}${username}:${key}`; + /** + * 包装key , 默认了username + * @param key + * @returns + */ + wrapperKey(key: string) { + const owner = this.owner; + return `${this.cachePrefix}${owner}:${key}`; } - async getConfig() { - return getChatConfig(); + static chatLimitKey(owner: string, key = 'chat-limit') { + return `ai-chat:model:${owner}:${key}`; + } + static async clearChatLimit(owner: string) { + const key = ChatServices.chatLimitKey(owner); + const cache = await redis.get(key); + if (cache) { + await redis.expire(key, 2); + } + return cache; + } + async getConfig(username: string) { + const services = new ChatConfigServices(this.owner, username); + return services.getChatConfig(); } - async chat(messages: ChatMessage[]) { + async chat(messages: ChatMessage[], options?: ChatMessageOptions, customOptions?: { clearThink?: boolean }) { const { model, provider, apiKey, baseURL } = this.modelConfig; - const providerManager = await ProviderManager.createProvider({ - provider: provider, - model: model, - apiKey: apiKey, - baseURL: baseURL, - }); - this.chatProvider = providerManager; - const result = await providerManager.chat(messages); - return result; + try { + const providerManager = await ProviderManager.createProvider({ + provider: provider, + model: model, + apiKey: apiKey, + baseURL: baseURL, + }); + this.chatProvider = providerManager; + const result = await providerManager.chat(messages, options); + const { clearThink = true } = customOptions || {}; + if (clearThink) { + result.choices[0].message.content = result.choices[0].message.content.replace(/[\s\S]*?<\/think>/g, ''); + } + return result; + } catch (error) { + log.error('chat error', { + errorMessage: error.message, + errorStack: error.stack, + provider, + model, + apiKey, + baseURL, + }); + throw error; + } } - static async createServices(opts: Partial) { - const username = opts.username || 'root'; - const model = opts.model || 'deepseek-r1-250120'; + async createTitle(messages: ChastHistoryMessage[]) { + return nanoid(); + } + /** + * 过滤消息,只保留对话需要的内容,name,role,content + * @param messages + * @returns + */ + async chatMessagePick(messages: ChastHistoryMessage[]) { + let newMessages = messages.filter((item) => !item.hide && !item.noUse); + return newMessages.map((item) => pick(item, ['role', 'content', 'name'])) as ChatMessage[]; + } + async createNewMessage(messages: ChastHistoryMessage[]) { + return messages.map((item) => { + if (!item.id) { + item.id = 'chat-' + nanoid(); + item.createdAt = Date.now(); + item.updatedAt = Date.now(); + } + return item; + }); + } + static async createServices(opts: Partial & { username: string }) { + const owner = opts.owner || 'root'; + const model = opts.model || 'deepseek-chat'; const group = opts.group || 'deepseek'; const decryptKey = opts.decryptKey; - return new ChatServices({ username, model, group, decryptKey }); + const chatServices = new ChatServices({ owner, model, group, decryptKey }); + await chatServices.init(opts.username); + return chatServices; + } + + /** + * 检查模型的余量 + * @returns + */ + async checkCanChat() { + const { modelConfig } = this; + const { tokenLimit, dayLimit, group, model } = modelConfig; + const key = this.wrapperKey(`chat-limit`); + try { + const cache = await redis.get(key); + if (cache) { + const cacheData = JSON.parse(cache); + const today = dayjs().format('YYYY-MM-DD'); + log.debug('checkCanChat', { cacheData }); + let current = cacheData.find((item) => item.group === group && item.model === model); + if (current) { + const day = current[today] || 0; + const token = current.token || 0; + if (tokenLimit && token >= tokenLimit) { + throw new CustomError(400, 'token limit exceeded'); + } + if (dayLimit && day >= dayLimit) { + throw new CustomError(400, 'day limit exceeded'); + } + } + } + return true; + } catch (error) { + console.error('checkCanChat error', error); + // 如果获取失败,则设置一个空的缓存,2秒后删除 + await redis.set(key, '', 'EX', 2); // 2秒 + throw new CustomError(500, 'checkCanChat error, please try again later'); + } + } + /** + * 获取模型的使用情况 + * @returns + */ + async getChatLimit() { + const { modelConfig } = this; + const { group, model } = modelConfig; + const key = this.wrapperKey(`chat-limit`); + const cache = await redis.get(key); + const today = dayjs().format('YYYY-MM-DD'); + if (cache) { + const cacheData = JSON.parse(cache); + return cacheData; + } + return [ + { + group: group, + model: model, + token: 0, + [today]: 0, + }, + ]; + } + /** + * 更新模型的使用情况 + * @param token + */ + async updateChatLimit(token: number) { + const { modelConfig } = this; + const { group, model } = modelConfig; + const key = this.wrapperKey(`chat-limit`); + const cache = await redis.get(key); + const today = dayjs().format('YYYY-MM-DD'); + try { + if (cache) { + const cacheData = JSON.parse(cache); + const current = cacheData.find((item) => item.group === group && item.model === model); + if (current) { + const day = current[today] || 0; + current[today] = day + 1; + current.token = current.token + token; + } else { + cacheData.push({ group, model, token: token, [today]: 1 }); + } + await redis.set(key, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天 + } else { + const cacheData = { group, model, token: token, [today]: 1 }; + await redis.set(key, JSON.stringify([cacheData]), 'EX', 60 * 60 * 24 * 30); // 30天 + } + } catch (error) { + console.error('updateChatLimit error', error); + // 如果更新失败,则设置一个空的缓存,2秒后删除 + await redis.set(key, '', 'EX', 2); // 2秒 + throw new CustomError(500, 'updateChatLimit error, please try again later'); + } } } diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..989a03e --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,3 @@ +import './ai-chat/index.ts'; +import './ai-chat/list.ts'; +import './ai-chat/cache.ts'; \ No newline at end of file diff --git a/src/test/encrypt/index.ts b/src/test/encrypt/index.ts new file mode 100644 index 0000000..7771358 --- /dev/null +++ b/src/test/encrypt/index.ts @@ -0,0 +1,9 @@ +import { encryptAES, decryptAES } from '../../provider/utils/parse-config.ts'; + +const plainx = process.env.API_KEY; +const decryptKey = process.env.DECRYPT_KEY; +const encrypt = encryptAES(plainx, decryptKey); +console.log('encrypt', encrypt); + +const decrypt = decryptAES(encrypt, decryptKey); +console.log(decrypt); diff --git a/src/test/model-scope/index.ts b/src/test/model-scope/index.ts new file mode 100644 index 0000000..c5a6b7e --- /dev/null +++ b/src/test/model-scope/index.ts @@ -0,0 +1,26 @@ +import { ModelScope } from '../../provider/chat-adapter/model-scope.ts'; +import { logInfo } from '../../logger/index.ts'; +import util from 'util'; +import { config } from 'dotenv'; +config(); + +const chat = new ModelScope({ + apiKey: process.env.MODEL_SCOPE_API_KEY, + model: 'Qwen/Qwen2.5-Coder-32B-Instruct', +}); + +// chat.chat([{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }]); +const chatMessage = [{ role: 'user', content: 'Hello, world! 1 + 1 equals ?' }]; + +const main = async () => { + const res = await chat.test(); + logInfo('test', res); +}; + +// main(); +const mainChat = async () => { + const res = await chat.chat(chatMessage as any); + logInfo('chat', res); +}; + +mainChat(); diff --git a/src/test/provider/index.ts b/src/test/provider/index.ts new file mode 100644 index 0000000..c596b2a --- /dev/null +++ b/src/test/provider/index.ts @@ -0,0 +1,6 @@ +import { ProviderManager } from '../../provider/index.ts'; + +const providerConfig = { provider: 'ModelScope', model: 'Qwen/Qwen2.5-Coder-32B-Instruct', apiKey: 'a4cc0e94-3633-4374-85a6-06f455e17bea' }; +const provider = await ProviderManager.createProvider(providerConfig); +const result = await provider.chat([{ role: 'user', content: '你好' }]); +console.log(result); diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts new file mode 100644 index 0000000..0f2f595 --- /dev/null +++ b/src/utils/uuid.ts @@ -0,0 +1,8 @@ +import { customAlphabet } from 'nanoid'; + +export const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; +export const nanoid = customAlphabet(alphabet, 16); + +export function uuid() { + return nanoid(); +} diff --git a/submodules/query-config b/submodules/query-config new file mode 160000 index 0000000..53cd974 --- /dev/null +++ b/submodules/query-config @@ -0,0 +1 @@ +Subproject commit 53cd97454dc4300a1a1040c25dd26d86c390f1c8