From 2ff8590cebe3a9acff1cc0785868929c067ce0fc Mon Sep 17 00:00:00 2001 From: xion Date: Sat, 5 Apr 2025 14:48:01 +0800 Subject: [PATCH 1/4] temp --- .gitignore | 2 + .gitmodules | 3 + package.json | 38 +- pnpm-lock.yaml | 434 ++++++++++++++++-- pnpm-workspace.yaml | 2 + rollup.config.mjs | 9 +- src/demo-route.ts | 18 +- src/dev.ts | 9 +- src/index.ts | 4 +- src/modules/db.ts | 15 +- src/modules/query.ts | 12 + src/modules/sequelize.ts | 30 ++ src/provider/core/type.ts | 3 - src/provider/utils/parse-config.ts | 21 +- src/routes/ai-chat/index.ts | 158 ++++++- src/routes/ai-chat/list.ts | 23 + src/routes/ai-chat/models/ai-chat-history.ts | 99 ++++ .../ai-chat/services/chat-config-srevices.ts | 128 ++++++ src/routes/ai-chat/services/chat-services.ts | 149 +++++- src/routes/index.ts | 2 + src/test/encrypt/index.ts | 9 + src/utils/uuid.ts | 8 + submodules/query-config | 1 + 23 files changed, 1079 insertions(+), 98 deletions(-) create mode 100644 .gitmodules create mode 100644 pnpm-workspace.yaml create mode 100644 src/modules/query.ts create mode 100644 src/modules/sequelize.ts create mode 100644 src/routes/ai-chat/list.ts create mode 100644 src/routes/ai-chat/models/ai-chat-history.ts create mode 100644 src/routes/ai-chat/services/chat-config-srevices.ts create mode 100644 src/routes/index.ts create mode 100644 src/test/encrypt/index.ts create mode 100644 src/utils/uuid.ts create mode 160000 submodules/query-config 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 e8d9f95..99ba59c 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,17 @@ { - "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" + ], "scripts": { "watch": "rollup -c rollup.config.mjs -w", "build": "rollup -c rollup.config.mjs", @@ -10,8 +19,7 @@ "test": "tsx test/**/*.ts", "dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ", "clean": "rm -rf dist", - "prepub": "envision switch root", - "pub": "npm run build && envision pack -p -u" + "pub": "envision pack -p -u" }, "keywords": [], "author": "abearxiong (https://www.xiongxiao.me)", @@ -19,18 +27,13 @@ "packageManager": "pnpm@10.7.1", "type": "module", "dependencies": { - "@kevisual/code-center-module": "0.0.18", - "@kevisual/mark": "0.0.7", - "@kevisual/router": "0.0.10", - "cookie": "^1.0.2", - "crypto-js": "^4.2.0", - "dayjs": "^1.11.13", - "formidable": "^3.5.2", - "json5": "^2.2.3", - "lodash-es": "^4.17.21", - "openai": "^4.91.1" + "@kevisual/router": "0.0.10" }, "devDependencies": { + "@kevisual/code-center-module": "0.0.18", + "@kevisual/mark": "0.0.7", + "@kevisual/query": "^0.0.15", + "@kevisual/query-config": "workspace:*", "@kevisual/types": "^0.0.6", "@kevisual/use-config": "^1.0.10", "@rollup/plugin-alias": "^5.1.1", @@ -45,11 +48,20 @@ "@types/node": "^22.14.0", "@vitejs/plugin-basic-ssl": "^2.0.0", "concurrently": "^9.1.2", + "cookie": "^1.0.2", "cross-env": "^7.0.3", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", "dotenv": "^16.4.7", + "formidable": "^3.5.2", "ioredis": "^5.6.0", "jsrepo": "^1.45.3", + "lodash-es": "^4.17.21", + "nanoid": "^5.1.5", "nodemon": "^3.1.9", + "openai": "^4.91.1", + "pg": "^8.14.1", + "pg-hstore": "^2.3.4", "pino": "^9.6.0", "pm2": "^6.0.5", "rimraf": "^6.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee24a79..7b6355d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,37 +8,22 @@ importers: .: dependencies: - '@kevisual/code-center-module': - specifier: 0.0.18 - version: 0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg@8.14.1)) - '@kevisual/mark': - specifier: 0.0.7 - version: 0.0.7(dotenv@16.4.7)(esbuild@0.25.2) '@kevisual/router': specifier: 0.0.10 version: 0.0.10 - cookie: - specifier: ^1.0.2 - version: 1.0.2 - crypto-js: - specifier: ^4.2.0 - version: 4.2.0 - dayjs: - specifier: ^1.11.13 - version: 1.11.13 - formidable: - specifier: ^3.5.2 - version: 3.5.2 - json5: - specifier: ^2.2.3 - version: 2.2.3 - lodash-es: - specifier: ^4.17.21 - version: 4.17.21 - openai: - specifier: ^4.91.1 - version: 4.91.1(ws@8.18.1)(zod@3.24.2) devDependencies: + '@kevisual/code-center-module': + specifier: 0.0.18 + version: 0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg-hstore@2.3.4)(pg@8.14.1)) + '@kevisual/mark': + specifier: 0.0.7 + version: 0.0.7(dotenv@16.4.7)(esbuild@0.25.2)(pg-hstore@2.3.4) + '@kevisual/query': + specifier: ^0.0.15 + version: 0.0.15(ws@8.18.1)(zod@3.24.2) + '@kevisual/query-config': + specifier: workspace:* + version: link:submodules/query-config '@kevisual/types': specifier: ^0.0.6 version: 0.0.6 @@ -81,21 +66,48 @@ importers: concurrently: specifier: ^9.1.2 version: 9.1.2 + cookie: + specifier: ^1.0.2 + version: 1.0.2 cross-env: specifier: ^7.0.3 version: 7.0.3 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 dotenv: specifier: ^16.4.7 version: 16.4.7 + formidable: + specifier: ^3.5.2 + version: 3.5.2 ioredis: specifier: ^5.6.0 version: 5.6.0 jsrepo: specifier: ^1.45.3 version: 1.45.3(typescript@5.8.2)(ws@8.18.1)(zod@3.24.2) + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 nodemon: specifier: ^3.1.9 version: 3.1.9 + openai: + specifier: ^4.91.1 + version: 4.91.1(ws@8.18.1)(zod@3.24.2) + pg: + specifier: ^8.14.1 + version: 8.14.1 + pg-hstore: + specifier: ^2.3.4 + version: 2.3.4 pino: specifier: ^9.6.0 version: 9.6.0 @@ -119,7 +131,7 @@ importers: version: 6.2.1(esbuild@0.25.2)(rollup@4.39.0) 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 @@ -136,6 +148,16 @@ importers: specifier: ^6.2.5 version: 6.2.5(@types/node@22.14.0)(tsx@4.19.3) + submodules/query-config: + dependencies: + '@kevisual/query': + specifier: ^0.0.13 + version: 0.0.13(ws@8.18.1)(zod@3.24.2) + devDependencies: + tsup: + specifier: ^8.4.0 + version: 8.4.0(postcss@8.5.3)(tsx@4.19.3)(typescript@5.8.2) + packages: '@ampproject/remapping@2.3.0': @@ -392,6 +414,12 @@ packages: '@kevisual/mark@0.0.7': resolution: {integrity: sha512-PiEEy4yvWEpixw76PzgrIWeNelzm+FrhtzFmqJU92o5GkgawaFwighcvIxqcVZRKeEFF4uvlTjFrGeQvXw6F4A==} + '@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 @@ -602,6 +630,10 @@ packages: '@oxc-project/types@0.60.0': resolution: {integrity: sha512-prhfNnb3ATFHOCv7mzKFfwLij5RzoUz6Y1n525ZhCEqfq5wreCXL+DyVoq3ShukPo7q45ZjYIdjFUgjj+WKzng==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pm2/agent@2.1.1': resolution: {integrity: sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==} @@ -963,6 +995,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'} @@ -1071,6 +1106,16 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1106,6 +1151,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1151,6 +1200,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==} @@ -1166,6 +1219,10 @@ packages: resolution: {integrity: sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w==} engines: {node: '>=18'} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1604,6 +1661,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} @@ -1862,10 +1923,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==} @@ -1888,11 +1956,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==} @@ -1900,6 +1963,17 @@ packages: resolution: {integrity: sha512-+eHkKSyrFJcJ0N/H57QAH3OsDja0T3zPN8DEti2ZyBL/4CZq/iR/BcEgyNgV2bFUvbR9/nUYbA33cgtztiQRsA==} hasBin: true + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -1912,9 +1986,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} @@ -1964,6 +2044,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==} @@ -1995,6 +2079,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} @@ -2145,6 +2232,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} @@ -2166,6 +2257,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'} @@ -2223,6 +2318,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'} @@ -2250,6 +2349,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} @@ -2301,6 +2418,10 @@ packages: pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2315,6 +2436,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'} @@ -2347,6 +2472,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==} @@ -2584,6 +2713,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 @@ -2644,6 +2777,11 @@ packages: stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -2674,12 +2812,26 @@ 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==} tiktoken@1.0.20: resolution: {integrity: sha512-zVIpXp84kth/Ni2me1uYlJgl2RZ2EjxwDaWLeDY/s6fZiyO9n1QoTOM5P7ZSYfToPvAvwYNMbg5LETVYVKyzfQ==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2698,16 +2850,41 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@1.9.3: resolution: {integrity: sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==} 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'} @@ -2756,6 +2933,9 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -2867,12 +3047,18 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + when-exit@2.1.4: resolution: {integrity: sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==} @@ -3161,7 +3347,7 @@ snapshots: '@kevisual/auth@1.0.5': {} - '@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg@8.14.1))': + '@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg-hstore@2.3.4)(pg@8.14.1))': dependencies: '@kevisual/auth': 1.0.5 '@kevisual/router': 0.0.10 @@ -3169,7 +3355,7 @@ snapshots: ioredis: 5.6.0 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: @@ -3181,7 +3367,7 @@ snapshots: dependencies: eventemitter3: 5.0.1 - '@kevisual/mark@0.0.7(dotenv@16.4.7)(esbuild@0.25.2)': + '@kevisual/mark@0.0.7(dotenv@16.4.7)(esbuild@0.25.2)(pg-hstore@2.3.4)': dependencies: '@kevisual/auth': 1.0.5 '@kevisual/rollup-tools': 0.0.1(esbuild@0.25.2) @@ -3190,7 +3376,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 @@ -3207,6 +3393,22 @@ snapshots: - tedious - utf-8-validate + '@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.39.0) @@ -3464,6 +3666,9 @@ snapshots: '@oxc-project/types@0.60.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@pm2/agent@2.1.1': dependencies: async: 3.2.6 @@ -3823,6 +4028,8 @@ snapshots: ansi-styles@6.2.1: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -3928,6 +4135,13 @@ snapshots: buffer-from@1.1.2: {} + bundle-require@5.1.0(esbuild@0.25.2): + dependencies: + esbuild: 0.25.2 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -3973,6 +4187,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + cli-boxes@3.0.0: {} cli-tableau@2.0.1: @@ -4007,6 +4225,8 @@ snapshots: commander@2.15.1: {} + commander@4.1.1: {} + commondir@1.0.1: {} concat-map@0.0.1: {} @@ -4033,6 +4253,8 @@ snapshots: semver: 7.7.1 uint8array-extras: 1.4.0 + consola@3.4.2: {} + cookie@0.7.2: {} cookie@1.0.2: {} @@ -4557,6 +4779,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 @@ -4831,10 +5062,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 @@ -4858,8 +5097,6 @@ snapshots: json-stringify-safe@5.0.1: optional: true - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -4906,6 +5143,12 @@ snapshots: - ws - zod + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + locate-character@3.0.0: {} lodash-es@4.17.21: {} @@ -4914,8 +5157,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: @@ -4957,6 +5204,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: {} @@ -4985,6 +5236,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: {} @@ -5152,6 +5409,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 @@ -5168,6 +5430,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): @@ -5233,6 +5499,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(supports-color@5.5.0) @@ -5308,6 +5576,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 @@ -5358,6 +5633,8 @@ snapshots: pstree.remy@1.1.8: {} + punycode@2.3.1: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -5370,6 +5647,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + real-require@0.2.0: {} redis-errors@1.2.0: {} @@ -5410,6 +5689,8 @@ snapshots: transitivePeerDependencies: - supports-color + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.10: @@ -5546,7 +5827,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 @@ -5566,6 +5847,7 @@ snapshots: wkx: 0.5.0 optionalDependencies: pg: 8.14.1 + pg-hstore: 2.3.4 transitivePeerDependencies: - supports-color @@ -5699,6 +5981,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: {} @@ -5767,6 +6053,16 @@ snapshots: stubborn-fs@1.2.5: {} + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -5826,12 +6122,27 @@ 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 tiktoken@1.0.20: {} + tinyexec@0.3.2: {} + + tinyglobby@0.2.12: + dependencies: + fdir: 6.4.3(picomatch@4.0.2) + picomatch: 4.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5844,12 +6155,45 @@ snapshots: tr46@0.0.3: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} + ts-interface-checker@0.1.13: {} + tslib@1.9.3: {} 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 @@ -5912,6 +6256,8 @@ snapshots: undefsafe@2.0.5: {} + underscore@1.13.7: {} + undici-types@5.26.5: {} undici-types@6.21.0: {} @@ -5974,6 +6320,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + whatwg-fetch@3.6.20: {} whatwg-url@5.0.0: @@ -5981,6 +6329,12 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + when-exit@2.1.4: {} which-boxed-primitive@1.1.1: 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/rollup.config.mjs b/rollup.config.mjs index d807dc7..04898d3 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -24,7 +24,7 @@ const config = { replace({ preventAssignment: true, // 防止意外赋值 DEV_SERVER: JSON.stringify(isDev), // 替换 process.env.NODE_ENV - VERSION: JSON.stringify(pkgs.version), + // VERSION: JSON.stringify(pkgs.version), }), alias({ // only esbuild needs to be configured @@ -54,6 +54,7 @@ const config = { }), resolve({ preferBuiltins: true, // 强制优先使用内置模块 + browser: false, }), commonjs(), esbuild({ @@ -67,9 +68,9 @@ const config = { /@kevisual\/router(\/.*)?/, //, // 路由 /@kevisual\/use-config(\/.*)?/, // - // 'sequelize', // 数据库 orm - // 'ioredis', // redis - // 'pg', // pg + 'sequelize', // 数据库 orm + 'ioredis', // redis + 'pg', // pg ], }; export default config; 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/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/core/type.ts b/src/provider/core/type.ts index 1edfc02..8866ca3 100644 --- a/src/provider/core/type.ts +++ b/src/provider/core/type.ts @@ -1,7 +1,4 @@ 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 ChatMessageOptions = Partial; diff --git a/src/provider/utils/parse-config.ts b/src/provider/utils/parse-config.ts index d74bc1a..e8d78df 100644 --- a/src/provider/utils/parse-config.ts +++ b/src/provider/utils/parse-config.ts @@ -1,14 +1,14 @@ -import { AES, enc } from 'crypto-js'; +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 +25,13 @@ type AIModel = { */ group: string; /** - * 每日限制 + * 每日请求频率限制 */ dayLimit?: number; + /** + * 总的token限制 + */ + tokenLimit?: number; }; type SecretKey = { @@ -56,6 +60,7 @@ export type ProviderResult = { group: string; apiKey: string; dayLimit?: number; + tokenLimit?: number; baseURL?: string; /** * 解密密钥 @@ -68,6 +73,12 @@ export type AIConfig = { description?: string; models: AIModel[]; secretKeys: SecretKey[]; + filter?: { + objectKey: string; + type: 'array' | 'object'; + operate: 'removeAttribute' | 'remove'; + attribute: string[]; + }[]; }; export class AIConfigParser { private config: AIConfig; diff --git a/src/routes/ai-chat/index.ts b/src/routes/ai-chat/index.ts index 2c891c2..082e08b 100644 --- a/src/routes/ai-chat/index.ts +++ b/src/routes/ai-chat/index.ts @@ -1,11 +1,165 @@ 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'; app .route({ path: 'ai', key: 'chat', + middleware: ['auth'], }) - .define(async () => { - // + .define(async (ctx) => { + const data = ctx.query.data || {}; + const { id, messages = [], options = {}, title, hook, getFull = false } = data; + let { username, model, group } = ctx.query; + const tokenUser = ctx.state.tokenUser || {}; + const tokenUsername = tokenUser.username; + let aiChatHistory: AiChatHistoryModel; + if (id) { + aiChatHistory = await AiChatHistoryModel.findByPk(id); + if (!aiChatHistory) { + ctx.throw(400, 'aiChatHistory not found'); + } + if (aiChatHistory.uid !== tokenUser.uid) { + ctx.throw(403, 'not permission'); + } + username = username || aiChatHistory.username; + model = model || aiChatHistory.model; + group = group || aiChatHistory.group; + } else { + username = 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, + }); + 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'); + } + const res = await chatServices.chat(pickMessages, options); + if (!aiChatHistory) { + aiChatHistory = await AiChatHistoryModel.create({ + username, + model, + group, + title, + }); + if (!title) { + // TODO: 创建标题 + } + } + const message = res.choices[0].message; + 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, + }; + if (hook) { + needUpdateData.data = { + ...aiChatHistory.data, + hook, + }; + } + await AiChatHistoryModel.update(needUpdateData, { where: { id: aiChatHistory.id } }); + ctx.body = { + message: newMessage[newMessage.length - 1], + aiChatHistory: getFull || !id ? aiChatHistory : undefined, + }; + }) + .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'], + }) + .define(async (ctx) => { + const username = ctx.query.username || 'root'; + const tokenUser = ctx.state.tokenUser; + const isSameUser = username === tokenUser.username; + const configObject: Record = {}; + const services = new ChatConfigServices(username, tokenUser.username); + const res = await services.getChatConfig(true, ctx.query.token); + configObject[username] = res; + if (!isSameUser) { + const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username); + const selfRes = await selfServices.getChatConfig(true, ctx.query.token); + configObject['self'] = selfRes; + } else { + configObject['self'] = res; + } + ctx.body = configObject; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'get-chat-usage', + description: '获取chat使用情况', + 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..b270d2f --- /dev/null +++ b/src/routes/ai-chat/list.ts @@ -0,0 +1,23 @@ +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 aiChatList = await AiChatHistoryModel.findAll({ + where: { + uid: tokenUser.uid, + }, + order: [['createdAt', 'DESC']], + }); + ctx.body = { + list: aiChatList, + }; + }) + .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..ce7b915 --- /dev/null +++ b/src/routes/ai-chat/models/ai-chat-history.ts @@ -0,0 +1,99 @@ +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 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 createdAt: Date; + declare updatedAt: Date; +} + +AiChatHistoryModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + }, + model: { + type: DataTypes.STRING, + allowNull: false, + }, + group: { + type: DataTypes.STRING, + allowNull: false, + }, + 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: {}, + }, + 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..9994f83 --- /dev/null +++ b/src/routes/ai-chat/services/chat-config-srevices.ts @@ -0,0 +1,128 @@ +import 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'; +export class ChatConfigServices { + cachePrefix = 'ai:chat:config'; + // 使用谁的模型 + owner: string; + // 使用者 + username: string; + /** + * username 是使用的模型的用户名,使用谁的模型 + * @param username + */ + constructor(owner: string, username: string, token?: string) { + this.owner = owner; + this.username = username; + } + getKey() { + return `${this.cachePrefix}:${this.owner}`; + } + /** + * 获取chat配置 + * @param needClearSecret 是否需要清除secret 默认false + * @returns + */ + async getChatConfig(needClearSecret = false, 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); + } + if (needClearSecret) { + modelConfig = this.filterApiKey(modelConfig); + } + return modelConfig; + } + 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天 + } + } +} diff --git a/src/routes/ai-chat/services/chat-services.ts b/src/routes/ai-chat/services/chat-services.ts index d98292b..e4969b4 100644 --- a/src/routes/ai-chat/services/chat-services.ts +++ b/src/routes/ai-chat/services/chat-services.ts @@ -1,9 +1,15 @@ 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 { 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'; + export type ChatServicesConfig = { - username: string; + owner: string; model: string; group: string; decryptKey?: string; @@ -13,7 +19,7 @@ export class ChatServices { /** * 用户名 */ - username: string; + owner: string; /** * 模型 */ @@ -32,7 +38,7 @@ export class ChatServices { modelConfig?: ProviderResult; 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 +47,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,24 +58,30 @@ 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 }; 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(); + async getConfig(username: string) { + const services = new ChatConfigServices(this.owner, username); + return services.getChatConfig(); } - async chat(messages: ChatMessage[]) { + async chat(messages: ChatMessage[], options?: ChatMessageOptions) { const { model, provider, apiKey, baseURL } = this.modelConfig; const providerManager = await ProviderManager.createProvider({ provider: provider, @@ -78,14 +90,111 @@ export class ChatServices { baseURL: baseURL, }); this.chatProvider = providerManager; - const result = await providerManager.chat(messages); + const result = await providerManager.chat(messages, options); return result; } - 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`); + const cache = await redis.get(key); + if (cache) { + const cacheData = JSON.parse(cache); + const today = dayjs().format('YYYY-MM-DD'); + const current = cacheData.find((item) => item.group === group && item.model === model); + 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; + } + /** + * 获取模型的使用情况 + * @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'); + 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 { + await redis.set(key, JSON.stringify({ group, model, token: token, [today]: 1 }), 'EX', 60 * 60 * 24 * 30); // 30天 + } } } diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..24dda09 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,2 @@ +import './ai-chat/index.ts'; +import './ai-chat/list.ts'; 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/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 From bfe8463212af50d661bd225d99b2378f5e8a33a3 Mon Sep 17 00:00:00 2001 From: xion Date: Sun, 6 Apr 2025 01:43:31 +0800 Subject: [PATCH 2/4] add permission check --- package.json | 2 + pnpm-lock.yaml | 31 +++++ src/provider/utils/parse-config.ts | 2 + src/routes/ai-chat/index.ts | 63 ++++++++-- src/routes/ai-chat/list.ts | 113 +++++++++++++++++- src/routes/ai-chat/models/ai-chat-history.ts | 9 ++ .../ai-chat/services/chat-config-srevices.ts | 3 + src/routes/ai-chat/services/chat-services.ts | 6 +- 8 files changed, 214 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 99ba59c..b36f3ab 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "packageManager": "pnpm@10.7.1", "type": "module", "dependencies": { + "@kevisual/cache": "^0.0.2", + "@kevisual/permission": "^0.0.1", "@kevisual/router": "0.0.10" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b6355d..d3f2c45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@kevisual/cache': + specifier: ^0.0.2 + version: 0.0.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2) + '@kevisual/permission': + specifier: ^0.0.1 + version: 0.0.1 '@kevisual/router': specifier: 0.0.10 version: 0.0.10 @@ -398,6 +404,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: @@ -414,6 +423,9 @@ 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==} @@ -1747,6 +1759,9 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} @@ -3347,6 +3362,18 @@ snapshots: '@kevisual/auth@1.0.5': {} + '@kevisual/cache@0.0.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2)': + dependencies: + '@rollup/plugin-commonjs': 28.0.3(rollup@4.39.0) + '@rollup/plugin-node-resolve': 16.0.1(rollup@4.39.0) + '@rollup/plugin-typescript': 12.1.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2) + idb-keyval: 6.2.1 + rollup-plugin-dts: 6.2.1(rollup@4.39.0)(typescript@5.8.2) + transitivePeerDependencies: + - rollup + - tslib + - typescript + '@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg-hstore@2.3.4)(pg@8.14.1))': dependencies: '@kevisual/auth': 1.0.5 @@ -3393,6 +3420,8 @@ 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) @@ -4882,6 +4911,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb-keyval@6.2.1: {} + ignore-by-default@1.0.1: {} ignore@5.3.2: {} diff --git a/src/provider/utils/parse-config.ts b/src/provider/utils/parse-config.ts index e8d78df..62a7b05 100644 --- a/src/provider/utils/parse-config.ts +++ b/src/provider/utils/parse-config.ts @@ -1,3 +1,4 @@ +import { Permission } from '@kevisual/permission'; import CryptoJS from 'crypto-js'; // 加密函数 @@ -73,6 +74,7 @@ export type AIConfig = { description?: string; models: AIModel[]; secretKeys: SecretKey[]; + permission?: Permission; filter?: { objectKey: string; type: 'array' | 'object'; diff --git a/src/routes/ai-chat/index.ts b/src/routes/ai-chat/index.ts index 082e08b..695f3de 100644 --- a/src/routes/ai-chat/index.ts +++ b/src/routes/ai-chat/index.ts @@ -2,7 +2,7 @@ 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'; app .route({ path: 'ai', @@ -21,19 +21,20 @@ app if (!aiChatHistory) { ctx.throw(400, 'aiChatHistory not found'); } - if (aiChatHistory.uid !== tokenUser.uid) { + if (aiChatHistory.uid !== tokenUser.id) { ctx.throw(403, 'not permission'); } - username = username || aiChatHistory.username; + 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, @@ -41,6 +42,15 @@ app 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(); @@ -72,6 +82,10 @@ app 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 = { @@ -126,19 +140,46 @@ app .define(async (ctx) => { const username = ctx.query.username || 'root'; const tokenUser = ctx.state.tokenUser; + const usernames = ctx.query.data?.usernames || []; + const tokenUsername = tokenUser.username; const isSameUser = username === tokenUser.username; - const configObject: Record = {}; + const configArray: any[] = []; const services = new ChatConfigServices(username, tokenUser.username); const res = await services.getChatConfig(true, ctx.query.token); - configObject[username] = res; + configArray.push({ + username, + config: res, + }); if (!isSameUser) { const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username); const selfRes = await selfServices.getChatConfig(true, ctx.query.token); - configObject['self'] = selfRes; - } else { - configObject['self'] = res; + configArray.push({ + username: tokenUser.username, + self: true, + config: selfRes, + }); } - ctx.body = configObject; + for (const username of usernames) { + const services = new ChatConfigServices(username, tokenUser.username); + const res = await services.getChatConfig(true, 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) { + // ctx.throw(403, `[${username}] ${checkPermission.message}`); + configArray.push({ + username, + config: null, + error: checkPermission.message, + }); + } else { + configArray.push({ + username, + config: res, + }); + } + } + ctx.body = configArray; }) .addTo(app); @@ -146,7 +187,7 @@ app .route({ path: 'ai', key: 'get-chat-usage', - description: '获取chat使用情况', + description: '获取chat使用情况, 只获取root的使用情况', middleware: ['auth'], }) .define(async (ctx) => { diff --git a/src/routes/ai-chat/list.ts b/src/routes/ai-chat/list.ts index b270d2f..cca9dd2 100644 --- a/src/routes/ai-chat/list.ts +++ b/src/routes/ai-chat/list.ts @@ -12,12 +12,121 @@ app const tokenUser = ctx.state.tokenUser; const aiChatList = await AiChatHistoryModel.findAll({ where: { - uid: tokenUser.uid, + uid: tokenUser.id, }, - order: [['createdAt', 'DESC']], + 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 index ce7b915..f0a4f80 100644 --- a/src/routes/ai-chat/models/ai-chat-history.ts +++ b/src/routes/ai-chat/models/ai-chat-history.ts @@ -33,6 +33,8 @@ export class AiChatHistoryModel extends Model { declare total_tokens: number; declare completion_tokens: number; + declare version: number; + declare createdAt: Date; declare updatedAt: Date; } @@ -47,14 +49,17 @@ AiChatHistoryModel.init( 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, @@ -82,6 +87,10 @@ AiChatHistoryModel.init( type: DataTypes.JSONB, defaultValue: {}, }, + version: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, uid: { type: DataTypes.UUID, allowNull: true, diff --git a/src/routes/ai-chat/services/chat-config-srevices.ts b/src/routes/ai-chat/services/chat-config-srevices.ts index 9994f83..746e618 100644 --- a/src/routes/ai-chat/services/chat-config-srevices.ts +++ b/src/routes/ai-chat/services/chat-config-srevices.ts @@ -8,6 +8,8 @@ export class ChatConfigServices { owner: string; // 使用者 username: string; + aiConfig?: AIConfig; + /** * username 是使用的模型的用户名,使用谁的模型 * @param username @@ -50,6 +52,7 @@ export class ChatConfigServices { const cacheTime = 60 * 60 * 24 * 40; // 1天 await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime); } + this.aiConfig = modelConfig; if (needClearSecret) { modelConfig = this.filterApiKey(modelConfig); } diff --git a/src/routes/ai-chat/services/chat-services.ts b/src/routes/ai-chat/services/chat-services.ts index e4969b4..774bc21 100644 --- a/src/routes/ai-chat/services/chat-services.ts +++ b/src/routes/ai-chat/services/chat-services.ts @@ -1,4 +1,4 @@ -import { AIConfigParser, ProviderResult } from '@/provider/utils/parse-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'; @@ -36,6 +36,7 @@ export class ChatServices { * 模型配置 */ modelConfig?: ProviderResult; + aiConfig?: AIConfig; chatProvider?: BaseChat; constructor(opts: ChatServicesConfig) { this.owner = opts.owner; @@ -65,6 +66,7 @@ export class ChatServices { }, }); that.modelConfig = { ...providerResult, apiKey }; + that.aiConfig = config; return that.modelConfig; } /** @@ -108,7 +110,7 @@ export class ChatServices { async createNewMessage(messages: ChastHistoryMessage[]) { return messages.map((item) => { if (!item.id) { - item.id = 'chat' + nanoid(); + item.id = 'chat-' + nanoid(); item.createdAt = Date.now(); item.updatedAt = Date.now(); } From 226f9a68968cbbfe68171413dffe1b08d0217c63 Mon Sep 17 00:00:00 2001 From: xion Date: Sun, 6 Apr 2025 23:25:46 +0800 Subject: [PATCH 3/4] feat: center change --- package.json | 6 +- pnpm-lock.yaml | 74 ++++++++ rollup.config.mjs | 3 + src/logger/index.ts | 37 ++++ src/provider/chat-adapter/custom.ts | 3 +- src/provider/chat-adapter/deepseek.ts | 3 +- src/provider/chat-adapter/model-scope.ts | 3 +- src/provider/chat-adapter/ollama.ts | 3 +- src/provider/chat-adapter/siliconflow.ts | 3 +- src/provider/chat-adapter/volces.ts | 3 +- src/provider/core/type.ts | 2 +- src/provider/index.ts | 2 + src/provider/utils/ai-config-type.ts | 52 ++++++ src/provider/utils/parse-config.ts | 48 ++++- src/routes/ai-chat/cache.ts | 43 +++++ src/routes/ai-chat/index.ts | 165 +++++++++++------- src/routes/ai-chat/list.ts | 2 + src/routes/ai-chat/models/ai-chat-history.ts | 6 + .../ai-chat/services/chat-config-srevices.ts | 35 +++- src/routes/ai-chat/services/chat-services.ts | 114 ++++++++---- src/routes/index.ts | 1 + src/test/model-scope/index.ts | 26 +++ src/test/provider/index.ts | 6 + 23 files changed, 521 insertions(+), 119 deletions(-) create mode 100644 src/logger/index.ts create mode 100644 src/provider/utils/ai-config-type.ts create mode 100644 src/routes/ai-chat/cache.ts create mode 100644 src/test/model-scope/index.ts create mode 100644 src/test/provider/index.ts diff --git a/package.json b/package.json index b36f3ab..590cc36 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "type": "system-app" }, "files": [ - "dist" + "dist", + "types" ], "scripts": { "watch": "rollup -c rollup.config.mjs -w", @@ -29,7 +30,8 @@ "dependencies": { "@kevisual/cache": "^0.0.2", "@kevisual/permission": "^0.0.1", - "@kevisual/router": "0.0.10" + "@kevisual/router": "0.0.10", + "pino-pretty": "^13.0.0" }, "devDependencies": { "@kevisual/code-center-module": "0.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3f2c45..5562649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@kevisual/router': specifier: 0.0.10 version: 0.0.10 + pino-pretty: + specifier: ^13.0.0 + version: 13.0.0 devDependencies: '@kevisual/code-center-module': specifier: 0.0.18 @@ -1197,6 +1200,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'} @@ -1291,6 +1297,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==} @@ -1400,6 +1409,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'} @@ -1523,6 +1535,9 @@ packages: fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1537,6 +1552,9 @@ packages: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} @@ -1736,6 +1754,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hexoid@2.0.0: resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} engines: {node: '>=8'} @@ -2326,6 +2347,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==} @@ -2433,6 +2458,9 @@ packages: pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2579,6 +2607,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'} @@ -2789,6 +2820,10 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} @@ -4244,6 +4279,8 @@ snapshots: colorette@1.4.0: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -4337,6 +4374,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: {} @@ -4443,6 +4482,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: @@ -4651,6 +4694,8 @@ snapshots: fast-content-type-parse@2.0.1: {} + fast-copy@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -4665,6 +4710,8 @@ snapshots: fast-redact@3.5.0: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.0.6: {} fastq@1.19.1: @@ -4885,6 +4932,8 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + hexoid@2.0.0: {} http-proxy-agent@7.0.2: @@ -5514,6 +5563,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: @@ -5664,6 +5729,11 @@ snapshots: pstree.remy@1.1.8: {} + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -5845,6 +5915,8 @@ snapshots: sax@1.4.1: {} + secure-json-parse@2.7.0: {} + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.11 @@ -6082,6 +6154,8 @@ snapshots: strip-final-newline@4.0.0: {} + strip-json-comments@3.1.1: {} + stubborn-fs@1.2.5: {} sucrase@3.35.0: diff --git a/rollup.config.mjs b/rollup.config.mjs index 04898d3..4ce8a3c 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -71,6 +71,9 @@ const config = { 'sequelize', // 数据库 orm 'ioredis', // redis 'pg', // pg + 'pino', // pino + 'pino-pretty', // pino-pretty + ], }; export default config; 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/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 8866ca3..01db2d6 100644 --- a/src/provider/core/type.ts +++ b/src/provider/core/type.ts @@ -1,6 +1,6 @@ import OpenAI from 'openai'; -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 62a7b05..05859cf 100644 --- a/src/provider/utils/parse-config.ts +++ b/src/provider/utils/parse-config.ts @@ -88,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); @@ -117,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) { @@ -144,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 695f3de..42034bc 100644 --- a/src/routes/ai-chat/index.ts +++ b/src/routes/ai-chat/index.ts @@ -3,6 +3,8 @@ 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', @@ -11,10 +13,13 @@ app }) .define(async (ctx) => { const data = ctx.query.data || {}; - const { id, messages = [], options = {}, title, hook, getFull = false } = data; - let { username, model, group } = ctx.query; + 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); @@ -58,46 +63,65 @@ app if (pickMessages.length === 0) { ctx.throw(400, 'chat messages is empty'); } - const res = await chatServices.chat(pickMessages, options); if (!aiChatHistory) { aiChatHistory = await AiChatHistoryModel.create({ username, model, group, title, + type: type || 'keep', }); if (!title) { // TODO: 创建标题 } } - const message = res.choices[0].message; - 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, - }; + 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); } - await AiChatHistoryModel.update(needUpdateData, { where: { id: aiChatHistory.id } }); - ctx.body = { - message: newMessage[newMessage.length - 1], - aiChatHistory: getFull || !id ? aiChatHistory : undefined, - }; }) .addTo(app); @@ -136,50 +160,69 @@ app 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[] = []; - const services = new ChatConfigServices(username, tokenUser.username); - const res = await services.getChatConfig(true, ctx.query.token); - configArray.push({ - username, - config: res, - }); - if (!isSameUser) { - const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username); - const selfRes = await selfServices.getChatConfig(true, ctx.query.token); - configArray.push({ - username: tokenUser.username, - self: true, - config: selfRes, - }); - } - for (const username of usernames) { + try { const services = new ChatConfigServices(username, tokenUser.username); - const res = await services.getChatConfig(true, 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) { - // ctx.throw(403, `[${username}] ${checkPermission.message}`); + 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, - config: null, - error: checkPermission.message, - }); - } else { - configArray.push({ - username, - config: res, + 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); } - ctx.body = configArray; }) .addTo(app); diff --git a/src/routes/ai-chat/list.ts b/src/routes/ai-chat/list.ts index cca9dd2..08c4bbb 100644 --- a/src/routes/ai-chat/list.ts +++ b/src/routes/ai-chat/list.ts @@ -10,9 +10,11 @@ app }) .define(async (ctx) => { const tokenUser = ctx.state.tokenUser; + const type = ctx.query.type || 'keep'; const aiChatList = await AiChatHistoryModel.findAll({ where: { uid: tokenUser.id, + type, }, order: [['updatedAt', 'DESC']], }); diff --git a/src/routes/ai-chat/models/ai-chat-history.ts b/src/routes/ai-chat/models/ai-chat-history.ts index f0a4f80..4e1777f 100644 --- a/src/routes/ai-chat/models/ai-chat-history.ts +++ b/src/routes/ai-chat/models/ai-chat-history.ts @@ -34,6 +34,7 @@ export class AiChatHistoryModel extends Model { declare completion_tokens: number; declare version: number; + declare type: string; declare createdAt: Date; declare updatedAt: Date; @@ -87,6 +88,11 @@ AiChatHistoryModel.init( type: DataTypes.JSONB, defaultValue: {}, }, + type: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'keep', // keep 保留 temp 临时 + }, version: { type: DataTypes.INTEGER, defaultValue: 0, diff --git a/src/routes/ai-chat/services/chat-config-srevices.ts b/src/routes/ai-chat/services/chat-config-srevices.ts index 746e618..3e7e51d 100644 --- a/src/routes/ai-chat/services/chat-config-srevices.ts +++ b/src/routes/ai-chat/services/chat-config-srevices.ts @@ -1,7 +1,8 @@ -import type { AIConfig } from '@/provider/utils/parse-config.ts'; +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'; // 使用谁的模型 @@ -9,7 +10,7 @@ export class ChatConfigServices { // 使用者 username: string; aiConfig?: AIConfig; - + isOwner: boolean; /** * username 是使用的模型的用户名,使用谁的模型 * @param username @@ -17,16 +18,17 @@ export class ChatConfigServices { 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 needClearSecret 是否需要清除secret 默认false + * @param keepSecret 是否需要清除secret 默认 不清除 为true * @returns */ - async getChatConfig(needClearSecret = false, token?: string) { + async getChatConfig(keepSecret = true, token?: string) { const key = this.getKey(); const cache = await redis.get(key); let modelConfig = null; @@ -35,7 +37,9 @@ export class ChatConfigServices { } 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`); + 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) { @@ -53,14 +57,26 @@ export class ChatConfigServices { await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime); } this.aiConfig = modelConfig; - if (needClearSecret) { + 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; + const { secretKeys = [], ...rest } = chatConfig; return { ...rest, secretKeys: secretKeys.map((item) => { @@ -128,4 +144,9 @@ export class ChatConfigServices { 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 774bc21..f02a75a 100644 --- a/src/routes/ai-chat/services/chat-services.ts +++ b/src/routes/ai-chat/services/chat-services.ts @@ -7,6 +7,7 @@ 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 = { owner: string; @@ -78,22 +79,49 @@ export class ChatServices { const owner = this.owner; return `${this.cachePrefix}${owner}:${key}`; } + 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[], options?: ChatMessageOptions) { + 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, options); - 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; + } } async createTitle(messages: ChastHistoryMessage[]) { return nanoid(); @@ -135,21 +163,31 @@ export class ChatServices { const { modelConfig } = this; const { tokenLimit, dayLimit, group, model } = modelConfig; const key = this.wrapperKey(`chat-limit`); - const cache = await redis.get(key); - if (cache) { - const cacheData = JSON.parse(cache); - const today = dayjs().format('YYYY-MM-DD'); - const current = cacheData.find((item) => item.group === group && item.model === model); - 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'); + 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'); } - return true; } /** * 获取模型的使用情况 @@ -184,19 +222,27 @@ export class ChatServices { 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); - 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; + 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 { - cacheData.push({ group, model, token: token, [today]: 1 }); + const cacheData = { group, model, token: token, [today]: 1 }; + await redis.set(key, JSON.stringify([cacheData]), 'EX', 60 * 60 * 24 * 30); // 30天 } - await redis.set(key, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天 - } else { - await redis.set(key, JSON.stringify({ group, model, token: token, [today]: 1 }), '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 index 24dda09..989a03e 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,2 +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/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); From fe716b8ea3512f4a1875dabdc070446bbbfc9adf Mon Sep 17 00:00:00 2001 From: xion Date: Mon, 7 Apr 2025 17:04:48 +0800 Subject: [PATCH 4/4] perf: perf request --- src/routes/ai-chat/list.ts | 7 ++++++- src/routes/ai-chat/models/ai-chat-history.ts | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/routes/ai-chat/list.ts b/src/routes/ai-chat/list.ts index 08c4bbb..07eaea4 100644 --- a/src/routes/ai-chat/list.ts +++ b/src/routes/ai-chat/list.ts @@ -11,10 +11,15 @@ app .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, - type, + ...search, }, order: [['updatedAt', 'DESC']], }); diff --git a/src/routes/ai-chat/models/ai-chat-history.ts b/src/routes/ai-chat/models/ai-chat-history.ts index 4e1777f..762ba59 100644 --- a/src/routes/ai-chat/models/ai-chat-history.ts +++ b/src/routes/ai-chat/models/ai-chat-history.ts @@ -18,6 +18,8 @@ type AiChatHistoryData = { [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;