From a225bd4f16a24fe18b2185f7214d97963da88bd4 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Fri, 4 Apr 2025 23:17:03 +0800 Subject: [PATCH] init demo --- .cursorignore | 2 + .gitignore | 7 +- index.html | 14 ++ package.json | 12 +- pnpm-lock.yaml | 178 +++++++++++++++++++ src/demo-route.ts | 4 +- src/dev.ts | 15 +- src/main.ts | 4 +- src/modules/chat-config.ts | 7 + src/modules/config.ts | 21 +++ src/modules/db.ts | 3 + src/modules/mark.ts | 9 - src/modules/sequelize.ts | 1 - src/modules/user.ts | 5 +- src/page/main.ts | 37 ++++ src/provider/chat-adapter/custom.ts | 12 ++ src/provider/chat-adapter/deepseek.ts | 8 + src/provider/chat-adapter/model-scope.ts | 9 + src/provider/chat-adapter/ollama.ts | 45 +++++ src/provider/chat-adapter/siliconflow.ts | 37 ++++ src/provider/chat-adapter/volces.ts | 8 + src/provider/core/chat.ts | 110 ++++++++++++ src/provider/core/index.ts | 2 + src/provider/core/text-regex.ts | 105 +++++++++++ src/provider/core/type.ts | 28 +++ src/provider/index.ts | 59 ++++++ src/provider/knowledge/knowledge-base.ts | 126 +++++++++++++ src/provider/knowledge/knowledge.ts | 7 + src/provider/utils/chunk.ts | 86 +++++++++ src/provider/utils/parse-config.ts | 141 +++++++++++++++ src/provider/utils/token.ts | 34 ++++ src/routes/ai-chat/index.ts | 11 ++ src/routes/ai-chat/services/chat-services.ts | 91 ++++++++++ src/routes/ai-manager/models/provider.ts | 78 -------- src/test/chunks/01-get.ts | 65 +++++++ src/test/ollama-knowledge.ts | 37 ++++ src/test/ollama.ts | 86 +++++++++ src/test/siliconflow/get.ts | 15 ++ vite.config.mjs | 9 + 39 files changed, 1426 insertions(+), 102 deletions(-) create mode 100644 .cursorignore create mode 100644 index.html create mode 100644 src/modules/chat-config.ts create mode 100644 src/modules/config.ts create mode 100644 src/modules/db.ts delete mode 100644 src/modules/mark.ts delete mode 100644 src/modules/sequelize.ts create mode 100644 src/page/main.ts create mode 100644 src/provider/chat-adapter/custom.ts create mode 100644 src/provider/chat-adapter/deepseek.ts create mode 100644 src/provider/chat-adapter/model-scope.ts create mode 100644 src/provider/chat-adapter/ollama.ts create mode 100644 src/provider/chat-adapter/siliconflow.ts create mode 100644 src/provider/chat-adapter/volces.ts create mode 100644 src/provider/core/chat.ts create mode 100644 src/provider/core/index.ts create mode 100644 src/provider/core/text-regex.ts create mode 100644 src/provider/core/type.ts create mode 100644 src/provider/index.ts create mode 100644 src/provider/knowledge/knowledge-base.ts create mode 100644 src/provider/knowledge/knowledge.ts create mode 100644 src/provider/utils/chunk.ts create mode 100644 src/provider/utils/parse-config.ts create mode 100644 src/provider/utils/token.ts create mode 100644 src/routes/ai-chat/index.ts create mode 100644 src/routes/ai-chat/services/chat-services.ts delete mode 100644 src/routes/ai-manager/models/provider.ts create mode 100644 src/test/chunks/01-get.ts create mode 100644 src/test/ollama-knowledge.ts create mode 100644 src/test/ollama.ts create mode 100644 src/test/siliconflow/get.ts create mode 100644 vite.config.mjs diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..ee5e8ff --- /dev/null +++ b/.cursorignore @@ -0,0 +1,2 @@ +config.json +.env* \ No newline at end of file diff --git a/.gitignore b/.gitignore index eae58d7..b1d32a4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,9 @@ cache-file /apps -logs \ No newline at end of file +logs + +.env* +!.env.example + +config.json diff --git a/index.html b/index.html new file mode 100644 index 0000000..a525a94 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Document + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 8229e50..e8d9f95 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "@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" + "lodash-es": "^4.17.21", + "openai": "^4.91.1" }, "devDependencies": { "@kevisual/types": "^0.0.6", @@ -41,10 +43,14 @@ "@types/formidable": "^3.4.5", "@types/lodash-es": "^4.17.12", "@types/node": "^22.14.0", + "@vitejs/plugin-basic-ssl": "^2.0.0", "concurrently": "^9.1.2", "cross-env": "^7.0.3", + "dotenv": "^16.4.7", + "ioredis": "^5.6.0", "jsrepo": "^1.45.3", "nodemon": "^3.1.9", + "pino": "^9.6.0", "pm2": "^6.0.5", "rimraf": "^6.0.1", "rollup": "^4.39.0", @@ -53,7 +59,9 @@ "rollup-plugin-esbuild": "^6.2.1", "sequelize": "^6.37.7", "tape": "^5.9.0", + "tiktoken": "^1.0.20", "tsx": "^4.19.3", - "typescript": "^5.8.2" + "typescript": "^5.8.2", + "vite": "^6.2.5" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07c1171..ee24a79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: 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 @@ -32,6 +35,9 @@ importers: 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/types': specifier: ^0.0.6 @@ -69,18 +75,30 @@ importers: '@types/node': specifier: ^22.14.0 version: 22.14.0 + '@vitejs/plugin-basic-ssl': + specifier: ^2.0.0 + version: 2.0.0(vite@6.2.5(@types/node@22.14.0)(tsx@4.19.3)) concurrently: specifier: ^9.1.2 version: 9.1.2 cross-env: specifier: ^7.0.3 version: 7.0.3 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + 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) nodemon: specifier: ^3.1.9 version: 3.1.9 + pino: + specifier: ^9.6.0 + version: 9.6.0 pm2: specifier: ^6.0.5 version: 6.0.5 @@ -105,12 +123,18 @@ importers: tape: specifier: ^5.9.0 version: 5.9.0 + tiktoken: + specifier: ^1.0.20 + version: 1.0.20 tsx: specifier: ^4.19.3 version: 4.19.3 typescript: specifier: ^5.8.2 version: 5.8.2 + vite: + specifier: ^6.2.5 + version: 6.2.5(@types/node@22.14.0)(tsx@4.19.3) packages: @@ -843,6 +867,12 @@ packages: '@types/validator@13.12.3': resolution: {integrity: sha512-2ipwZ2NydGQJImne+FhNdhgRM37e9lCev99KnqkbFHd94Xn/mErARWI1RSLem1QA19ch5kOhzIZd7e8CA2FI8g==} + '@vitejs/plugin-basic-ssl@2.0.0': + resolution: {integrity: sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + peerDependencies: + vite: ^6.0.0 + '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -980,6 +1010,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + atomically@2.0.3: resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} @@ -1156,6 +1190,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-dependency@0.0.3: resolution: {integrity: sha512-jLQuve6jhpjkH3+k2Y8jK3j27Hm3rnIsRW/8oOf9oxFOBI5iu6sndwSv6lj5dNfO9JVP6cNb8Xs+VXhndgtLfQ==} @@ -1427,6 +1464,10 @@ packages: fast-json-patch@3.1.1: resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} @@ -2038,6 +2079,10 @@ packages: ollama@0.5.14: resolution: {integrity: sha512-pvOuEYa2WkkAumxzJP0RdEYHkbZ64AYyyUszXVX7ruLvk5L+EiO2G71da2GqEQ4IAk4j6eLoUbGk5arzFT1wJA==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2168,6 +2213,16 @@ packages: resolution: {integrity: sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==} engines: {node: '>=10'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.6.0: + resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} + hasBin: true + pm2-axon-rpc@0.7.1: resolution: {integrity: sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==} engines: {node: '>=5'} @@ -2230,6 +2285,9 @@ packages: resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + promptly@2.2.0: resolution: {integrity: sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==} @@ -2246,6 +2304,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + read@1.0.7: resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} engines: {node: '>=0.8'} @@ -2254,6 +2315,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -2360,6 +2425,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2501,6 +2570,9 @@ packages: resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2602,6 +2674,12 @@ packages: resolution: {integrity: sha512-czbGgxSVwRlbB3Ly/aqQrNwrDAzKHDW/kVXegp4hSFmR2c8qqm3hCgZbUy1+3QAQFGhPDG7J56UsV1uNilBFCA==} hasBin: true + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tiktoken@1.0.20: + resolution: {integrity: sha512-zVIpXp84kth/Ni2me1uYlJgl2RZ2EjxwDaWLeDY/s6fZiyO9n1QoTOM5P7ZSYfToPvAvwYNMbg5LETVYVKyzfQ==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2726,6 +2804,46 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite@6.2.5: + resolution: {integrity: sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vizion@2.2.1: resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} engines: {node: '>=4.0'} @@ -3597,6 +3715,10 @@ snapshots: '@types/validator@13.12.3': {} + '@vitejs/plugin-basic-ssl@2.0.0(vite@6.2.5(@types/node@22.14.0)(tsx@4.19.3))': + dependencies: + vite: 6.2.5(@types/node@22.14.0)(tsx@4.19.3) + '@vue/compiler-core@3.5.13': dependencies: '@babel/parser': 7.27.0 @@ -3751,6 +3873,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + atomically@2.0.3: dependencies: stubborn-fs: 1.2.5 @@ -3930,6 +4054,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + css-dependency@0.0.3: dependencies: ansi-regex: 6.1.0 @@ -4286,6 +4412,8 @@ snapshots: fast-json-patch@3.1.1: {} + fast-redact@3.5.0: {} + fast-uri@3.0.6: {} fastq@1.19.1: @@ -4944,6 +5072,8 @@ snapshots: dependencies: whatwg-fetch: 3.6.20 + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -5083,6 +5213,26 @@ snapshots: dependencies: safe-buffer: 5.2.1 + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.6.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.1 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pm2-axon-rpc@0.7.1: dependencies: debug: 4.4.0(supports-color@5.5.0) @@ -5185,6 +5335,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-warning@4.0.1: {} + promptly@2.2.0: dependencies: read: 1.0.7 @@ -5208,6 +5360,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + read@1.0.7: dependencies: mute-stream: 0.0.8 @@ -5216,6 +5370,8 @@ snapshots: dependencies: picomatch: 2.3.1 + real-require@0.2.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -5371,6 +5527,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.4.1: {} @@ -5528,6 +5686,10 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5664,6 +5826,12 @@ snapshots: resolve: 2.0.0-next.5 string.prototype.trim: 1.2.10 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tiktoken@1.0.20: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5773,6 +5941,16 @@ snapshots: vary@1.1.2: {} + vite@6.2.5(@types/node@22.14.0)(tsx@4.19.3): + dependencies: + esbuild: 0.25.2 + postcss: 8.5.3 + rollup: 4.39.0 + optionalDependencies: + '@types/node': 22.14.0 + fsevents: 2.3.3 + tsx: 4.19.3 + vizion@2.2.1: dependencies: async: 2.6.4 diff --git a/src/demo-route.ts b/src/demo-route.ts index 651a013..aabbc93 100644 --- a/src/demo-route.ts +++ b/src/demo-route.ts @@ -1,5 +1,5 @@ import { app } from './app.ts'; -import { useConfig } from '@kevisual/use-config'; +import { useConfig } from '@kevisual/use-config/env'; app .route({ @@ -13,4 +13,4 @@ app const config = useConfig(); -console.log('run demo: http://localhost:' + config.port + '/api/router?path=demo&key=demo'); +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 818c369..d089c88 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -1,8 +1,15 @@ -import { useConfig } from '@kevisual/use-config'; -import { app } from './index.ts'; +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'; // 开发环境 const config = useConfig(); -app.listen(config.port, () => { - console.log(`server is running at http://localhost:${config.port}`); +const port = config.PORT || 6666; + +app.listen(port, () => { + console.log(`server is running at http://localhost:${port}`); }); diff --git a/src/main.ts b/src/main.ts index 6e431cb..4d18c83 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,10 @@ // 单应用实例启动 -import { useConfig } from '@kevisual/use-config'; +import { useConfig } from '@kevisual/use-config/env'; import { app } from './index.ts'; const config = useConfig(); -app.listen(config.port, () => { +app.listen(config.PORT, () => { console.log(`server is running at http://localhost:${config.port}`); }); diff --git a/src/modules/chat-config.ts b/src/modules/chat-config.ts new file mode 100644 index 0000000..a012527 --- /dev/null +++ b/src/modules/chat-config.ts @@ -0,0 +1,7 @@ +import fs from 'fs'; +import path from 'path'; + +export const getChatConfig = () => { + const config = fs.readFileSync(path.resolve(process.cwd(), 'config.json'), 'utf-8'); + return JSON.parse(config); +}; diff --git a/src/modules/config.ts b/src/modules/config.ts new file mode 100644 index 0000000..6f469ee --- /dev/null +++ b/src/modules/config.ts @@ -0,0 +1,21 @@ +import { useConfig } from '@kevisual/use-config/env'; +export const envConfig = useConfig() as any; +type ConfigType = { + /** + * 主机 http://localhost:3000 + */ + host: string; + /** + * 路径 /api + */ + path: string; + /** + * 端口 + */ + port: number; +}; +export const config: ConfigType = { + host: envConfig.API_HOST || 'http://localhost:4005', + path: envConfig.API_PATH || '/api/router', + port: envConfig.PORT || 6666, +}; diff --git a/src/modules/db.ts b/src/modules/db.ts new file mode 100644 index 0000000..0ec1fb5 --- /dev/null +++ b/src/modules/db.ts @@ -0,0 +1,3 @@ +import { useContextKey } from '@kevisual/use-config/context'; + +export const redis = useContextKey('redis'); diff --git a/src/modules/mark.ts b/src/modules/mark.ts deleted file mode 100644 index ee41462..0000000 --- a/src/modules/mark.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Mark, markModelInit } from '@kevisual/mark'; - -export { Mark, markModelInit }; - -export const init = () => { - markModelInit({ - tableName: '', - }); -}; diff --git a/src/modules/sequelize.ts b/src/modules/sequelize.ts deleted file mode 100644 index 4c59901..0000000 --- a/src/modules/sequelize.ts +++ /dev/null @@ -1 +0,0 @@ -export { sequelize, redis } from '@kevisual/code-center-module'; diff --git a/src/modules/user.ts b/src/modules/user.ts index c628337..0fdcd99 100644 --- a/src/modules/user.ts +++ b/src/modules/user.ts @@ -1,6 +1,5 @@ -import { sequelize, User, UserInit, Org, OrgInit } from '@kevisual/code-center-module'; - -export { sequelize, User, UserInit, Org, OrgInit }; +import { User, UserInit, Org, OrgInit } from '@kevisual/code-center-module/models'; +export { User, Org }; export const init = () => { UserInit(); diff --git a/src/page/main.ts b/src/page/main.ts new file mode 100644 index 0000000..fb0d6ae --- /dev/null +++ b/src/page/main.ts @@ -0,0 +1,37 @@ +import { BaseChat } from '../provider/index.ts'; +import { Knowledge } from '../provider/knowledge/knowledge-base.ts'; +const chat = new BaseChat({ + baseURL: 'https://ollama.xiongxiao.me/v1', + apiKey: '', + model: 'qwq:latest', + stream: false, + isBrowser: true, +}); + +// chat.chat([{ role: 'user', content: 'Hello, world!' }]); + +const main = async () => { + const res = await chat.test(); +}; + +// main(); +// @ts-ignore +window.main = main; + +const knowledge = new Knowledge({ + embeddingModel: 'bge-m3:latest', + baseURL: 'https://ollama.xiongxiao.me/v1', + model: 'qwq:latest', + apiKey: '', + isBrowser: true, +}); + +const createEmbedding = async () => { + const res = await knowledge.generateEmbeddingCore('Hello, world! this is a test'); + console.log('res', res.data[0].embedding.length); + console.log(res); +}; + +// createEmbedding(); +// @ts-ignore +window.createEmbedding = createEmbedding; \ No newline at end of file diff --git a/src/provider/chat-adapter/custom.ts b/src/provider/chat-adapter/custom.ts new file mode 100644 index 0000000..ffae8df --- /dev/null +++ b/src/provider/chat-adapter/custom.ts @@ -0,0 +1,12 @@ +import { BaseChat, BaseChatOptions } from '../core/chat.ts'; + +export type OllamaOptions = BaseChatOptions; + +/** + * 自定义模型 + */ +export class Custom extends BaseChat { + constructor(options: OllamaOptions) { + super(options); + } +} diff --git a/src/provider/chat-adapter/deepseek.ts b/src/provider/chat-adapter/deepseek.ts new file mode 100644 index 0000000..4508708 --- /dev/null +++ b/src/provider/chat-adapter/deepseek.ts @@ -0,0 +1,8 @@ +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); + } +} diff --git a/src/provider/chat-adapter/model-scope.ts b/src/provider/chat-adapter/model-scope.ts new file mode 100644 index 0000000..4bb8b04 --- /dev/null +++ b/src/provider/chat-adapter/model-scope.ts @@ -0,0 +1,9 @@ +// https://api-inference.modelscope.cn/v1/ +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); + } +} diff --git a/src/provider/chat-adapter/ollama.ts b/src/provider/chat-adapter/ollama.ts new file mode 100644 index 0000000..bb4e792 --- /dev/null +++ b/src/provider/chat-adapter/ollama.ts @@ -0,0 +1,45 @@ +import { BaseChat, BaseChatOptions } from '../core/index.ts'; +import type { ChatMessage, ChatMessageOptions } from '../core/index.ts'; + +export type OllamaOptions = Partial; + +type OllamaModel = { + name: string; + model: string; + modified_at: string; + + size: number; + digest: string; + details: { + parent_model: string; + format: string; // example: gguf + family: string; // example qwen + families: string[]; + parameter_size: string; + quantization_level: string; // example: Q4_K_M Q4_0 + }; +}; +export class Ollama extends BaseChat { + constructor(options: OllamaOptions) { + super({ baseURL: 'http://localhost:11434/v1', ...(options as BaseChatOptions) }); + } + async chat(messages: ChatMessage[], options?: ChatMessageOptions) { + const res = await super.chat(messages, options); + console.log('thunk', this.getChatUsage()); + return res; + } + /** + * 获取模型列表 + * @returns + */ + async listModels(): Promise<{ models: OllamaModel[] }> { + const _url = new URL(this.baseURL); + const tagsURL = new URL('/api/tags', _url); + return this.openai.get(tagsURL.toString()); + } + async listRunModels(): Promise<{ models: OllamaModel[] }> { + const _url = new URL(this.baseURL); + const tagsURL = new URL('/api/ps', _url); + return this.openai.get(tagsURL.toString()); + } +} diff --git a/src/provider/chat-adapter/siliconflow.ts b/src/provider/chat-adapter/siliconflow.ts new file mode 100644 index 0000000..7948e5f --- /dev/null +++ b/src/provider/chat-adapter/siliconflow.ts @@ -0,0 +1,37 @@ +import { BaseChat, BaseChatOptions } from '../core/chat.ts'; +import { OpenAI } from 'openai'; + +type SiliconFlowOptions = Partial; + +type SiliconFlowUsageData = { + id: string; + name: string; + image: string; + email: string; + isAdmin: boolean; + balance: string; + status: 'normal' | 'suspended' | 'expired' | string; // 状态 + introduce: string; + role: string; + chargeBalance: string; + totalBalance: string; + category: string; +}; +type SiliconFlowUsageResponse = { + code: number; + message: string; + status: boolean; + data: SiliconFlowUsageData; +}; +export class SiliconFlow extends BaseChat { + constructor(options: SiliconFlowOptions) { + super({ baseURL: 'https://api.siliconflow.com/v1', ...(options as BaseChatOptions) }); + } + async getUsageInfo(): Promise { + return this.openai.get('/user/info'); + } + async chat(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], options?: Partial) { + const res = await super.chat(messages, options); + return res; + } +} diff --git a/src/provider/chat-adapter/volces.ts b/src/provider/chat-adapter/volces.ts new file mode 100644 index 0000000..7ffe707 --- /dev/null +++ b/src/provider/chat-adapter/volces.ts @@ -0,0 +1,8 @@ +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); + } +} diff --git a/src/provider/core/chat.ts b/src/provider/core/chat.ts new file mode 100644 index 0000000..359db92 --- /dev/null +++ b/src/provider/core/chat.ts @@ -0,0 +1,110 @@ +import { OpenAI } from 'openai'; +import type { BaseChatInterface, ChatMessageComplete, ChatMessage, ChatMessageOptions, BaseChatUsageInterface } from './type.ts'; + +export type BaseChatOptions> = { + /** + * 默认baseURL + */ + baseURL: string; + /** + * 默认模型 + */ + model: string; + /** + * 默认apiKey + */ + apiKey: string; + /** + * 是否在浏览器中使用 + */ + isBrowser?: boolean; + /** + * 是否流式输出, 默认 false + */ + stream?: boolean; +} & T; + +export class BaseChat implements BaseChatInterface, BaseChatUsageInterface { + /** + * 默认baseURL + */ + baseURL: string; + /** + * 默认模型 + */ + model: string; + /** + * 默认apiKey + */ + apiKey: string; + /** + * 是否在浏览器中使用 + */ + isBrowser: boolean; + /** + * openai实例 + */ + openai: OpenAI; + + prompt_tokens: number; + total_tokens: number; + completion_tokens: number; + + constructor(options: BaseChatOptions) { + this.baseURL = options.baseURL; + this.model = options.model; + this.apiKey = options.apiKey; + this.isBrowser = options.isBrowser ?? false; + this.openai = new OpenAI({ + apiKey: this.apiKey, + baseURL: this.baseURL, + dangerouslyAllowBrowser: this.isBrowser, + }); + } + /** + * 聊天 + */ + async chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise { + const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = { + model: this.model, + messages, + ...options, + stream: false, + }; + const res = (await this.openai.chat.completions.create(createParams)) as ChatMessageComplete; + this.prompt_tokens = res.usage?.prompt_tokens ?? 0; + this.total_tokens = res.usage?.total_tokens ?? 0; + this.completion_tokens = res.usage?.completion_tokens ?? 0; + return res; + } + async chatStream(messages: ChatMessage[], options?: ChatMessageOptions) { + const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = { + model: this.model, + messages, + ...options, + stream: true, + }; + if (createParams.response_format) { + throw new Error('response_format is not supported in stream mode'); + } + return this.openai.chat.completions.create(createParams) as any; + } + + /** + * 测试 + */ + test() { + return this.chat([{ role: 'user', content: 'Hello, world!' }]); + } + /** + * 获取聊天使用情况 + * @returns + */ + getChatUsage() { + return { + prompt_tokens: this.prompt_tokens, + total_tokens: this.total_tokens, + completion_tokens: this.completion_tokens, + }; + } +} diff --git a/src/provider/core/index.ts b/src/provider/core/index.ts new file mode 100644 index 0000000..476437a --- /dev/null +++ b/src/provider/core/index.ts @@ -0,0 +1,2 @@ +export * from './chat.ts'; +export * from './type.ts'; diff --git a/src/provider/core/text-regex.ts b/src/provider/core/text-regex.ts new file mode 100644 index 0000000..834e420 --- /dev/null +++ b/src/provider/core/text-regex.ts @@ -0,0 +1,105 @@ +// Updated: Aug. 20, 2024 +// Live demo: https://jina.ai/tokenizer +// LICENSE: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0) +// COPYRIGHT: Jina AI + +// Define variables for magic numbers +const MAX_HEADING_LENGTH = 7; +const MAX_HEADING_CONTENT_LENGTH = 200; +const MAX_HEADING_UNDERLINE_LENGTH = 200; +const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100; +const MAX_LIST_ITEM_LENGTH = 200; +const MAX_NESTED_LIST_ITEMS = 6; +const MAX_LIST_INDENT_SPACES = 7; +const MAX_BLOCKQUOTE_LINE_LENGTH = 200; +const MAX_BLOCKQUOTE_LINES = 15; +const MAX_CODE_BLOCK_LENGTH = 1500; +const MAX_CODE_LANGUAGE_LENGTH = 20; +const MAX_INDENTED_CODE_LINES = 20; +const MAX_TABLE_CELL_LENGTH = 200; +const MAX_TABLE_ROWS = 20; +const MAX_HTML_TABLE_LENGTH = 2000; +const MIN_HORIZONTAL_RULE_LENGTH = 3; +const MAX_SENTENCE_LENGTH = 400; +const MAX_QUOTED_TEXT_LENGTH = 300; +const MAX_PARENTHETICAL_CONTENT_LENGTH = 200; +const MAX_NESTED_PARENTHESES = 5; +const MAX_MATH_INLINE_LENGTH = 100; +const MAX_MATH_BLOCK_LENGTH = 500; +const MAX_PARAGRAPH_LENGTH = 1000; +const MAX_STANDALONE_LINE_LENGTH = 800; +const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100; +const MAX_HTML_TAG_CONTENT_LENGTH = 1000; +const LOOKAHEAD_RANGE = 100; // Number of characters to look ahead for a sentence boundary + +const AVOID_AT_START = `[\\s\\]})>,']`; +const PUNCTUATION = `[.!?…]|\\.{3}|[\\u2026\\u2047-\\u2049]|[\\p{Emoji_Presentation}\\p{Extended_Pictographic}]`; +const QUOTE_END = `(?:'(?=\`)|''(?=\`\`))`; +const SENTENCE_END = `(?:${PUNCTUATION}(?]{0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\\r\\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:)?(?:\\r?\\n|$))` + + "|" + + // New pattern for citations + `(?:\\[[0-9]+\\][^\\r\\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` + + "|" + + // 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints) + `(?:(?:^|\\r?\\n)[ \\t]{0,3}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}` + + `(?:(?:\\r?\\n[ \\t]{2,5}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` + + `(?:\\r?\\n[ \\t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` + + "|" + + // 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints) + `(?:(?:^>(?:>|\\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\\r?\\n?){1,${MAX_BLOCKQUOTE_LINES}})` + + "|" + + // 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints) + `(?:(?:^|\\r?\\n)(?:\`\`\`|~~~)(?:\\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\\r?\\n[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:\`\`\`|~~~)\\r?\\n?` + + `|(?:(?:^|\\r?\\n)(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\\r?\\n(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,${MAX_INDENTED_CODE_LINES}}\\r?\\n?)` + + `|(?:
(?:)?[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:)?
))` + + "|" + + // 5. Tables (Markdown, grid tables, and HTML tables, with length constraints) + `(?:(?:^|\\r?\\n)(?:\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|(?:\\r?\\n\\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\\|){0,1}(?:\\r?\\n\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|){0,${MAX_TABLE_ROWS}}` + + `|[\\s\\S]{0,${MAX_HTML_TABLE_LENGTH}}?
))` + + "|" + + // 6. Horizontal rules (Markdown and HTML hr tag) + `(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\\s*$|)` + + "|" + + // 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints) + `(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}(?:)?(?:\\r?\\n|$))` + + "|" + + // 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation) + `(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` + + "|" + + // 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints) + "(?:" + + `(?)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:

)?(?=\\r?\\n\\r?\\n|$))` + + "|" + + // 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints) + `(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\\s\\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?|\\s*/>))` + + "|" + + // 12. LaTeX-style math expressions (inline and block, with length constraints) + `(?:(?:\\$\\$[\\s\\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\\$\\$)|(?:\\$[^\\$\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\\$))` + + "|" + + // 14. Fallback for any remaining content (with length constraints) + `(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` + + ")", + "gmu" +); + diff --git a/src/provider/core/type.ts b/src/provider/core/type.ts new file mode 100644 index 0000000..1edfc02 --- /dev/null +++ b/src/provider/core/type.ts @@ -0,0 +1,28 @@ +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; +export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion; +export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion; + +export interface BaseChatInterface { + chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise; +} + +export interface BaseChatUsageInterface { + /** + * 提示词令牌 + */ + prompt_tokens: number; + /** + * 总令牌 + */ + total_tokens: number; + /** + * 完成令牌 + */ + completion_tokens: number; +} diff --git a/src/provider/index.ts b/src/provider/index.ts new file mode 100644 index 0000000..c21bb04 --- /dev/null +++ b/src/provider/index.ts @@ -0,0 +1,59 @@ +export * from './core/index.ts'; +import { BaseChat } from './core/chat.ts'; + +import { Ollama } from './chat-adapter/ollama.ts'; +import { SiliconFlow } from './chat-adapter/siliconflow.ts'; +import { Custom } from './chat-adapter/custom.ts'; +import { Volces } from './chat-adapter/volces.ts'; +import { DeepSeek } from './chat-adapter/deepseek.ts'; +import { ModelScope } from './chat-adapter/model-scope.ts'; +import { ChatMessage } from './core/type.ts'; + +export const OllamaProvider = Ollama; +export const SiliconFlowProvider = SiliconFlow; +export const CustomProvider = Custom; +export const VolcesProvider = Volces; +export const DeepSeekProvider = DeepSeek; +export const ModelScopeProvider = ModelScope; + +export const ProviderMap = { + Ollama: OllamaProvider, + SiliconFlow: SiliconFlowProvider, + Custom: CustomProvider, + Volces: VolcesProvider, + DeepSeek: DeepSeekProvider, + ModelScope: ModelScopeProvider, + BaseChat: BaseChat, +}; + +type ProviderManagerConfig = { + provider: string; + model: string; + apiKey: string; + baseURL?: string; +}; +export class ProviderManager { + provider: BaseChat; + constructor(config: ProviderManagerConfig) { + const { provider, model, apiKey, baseURL } = config; + const Provider = ProviderMap[provider] as typeof BaseChat; + if (!Provider) { + throw new Error(`Provider ${provider} not found`); + } + this.provider = new Provider({ + model, + apiKey, + baseURL, + }); + } + static async createProvider(config: ProviderManagerConfig) { + if (!config.baseURL) { + delete config.baseURL; + } + const pm = new ProviderManager(config); + return pm.provider; + } + async chat(messages: ChatMessage[]) { + return this.provider.chat(messages); + } +} diff --git a/src/provider/knowledge/knowledge-base.ts b/src/provider/knowledge/knowledge-base.ts new file mode 100644 index 0000000..7706831 --- /dev/null +++ b/src/provider/knowledge/knowledge-base.ts @@ -0,0 +1,126 @@ +import { BaseChat, BaseChatOptions } from '../core/chat.ts'; +import { numTokensFromString } from '../utils/token.ts'; + +export type KnowledgeOptions> = BaseChatOptions< + { + embeddingModel: string; + splitSize?: number; // 分块大小 默认 2000 + splitOverlap?: number; // 分块重叠 默认 200 + batchSize?: number; // 批量大小 默认 4, 4*2000=8000 + } & T +>; +/** + * 知识库构建 + * 1. Embedding generate + * 2. retriever + * 3. reranker + */ +export class KnowledgeBase extends BaseChat { + embeddingModel: string; + splitSize: number; + splitOverlap: number; + batchSize: number; + constructor(options: KnowledgeOptions) { + super(options); + this.embeddingModel = options.embeddingModel; + this.splitSize = options.splitSize || 2000; + this.splitOverlap = options.splitOverlap || 200; + this.prompt_tokens = 0; + this.total_tokens = 0; + this.batchSize = options.batchSize || 4; + } + /** + * 生成embedding 内部 + * @param text + * @returns + */ + async generateEmbeddingCore(text: string | string[]) { + const res = await this.openai.embeddings.create({ + model: this.embeddingModel, + input: text, + encoding_format: 'float', + }); + this.prompt_tokens += res.usage.prompt_tokens; + this.total_tokens += res.usage.total_tokens; + return res; + } + async generateEmbeddingBatchCore(text: string[]) { + const res = await this.openai.embeddings.create({ + model: this.embeddingModel, + input: text, + encoding_format: 'float', + }); + this.prompt_tokens += res.usage.prompt_tokens; + this.total_tokens += res.usage.total_tokens; + return res.data.map((item) => item.embedding); + } + /** + * 生成embedding + * @param text + * @returns + */ + async generateEmbedding(text: string | string[]) { + if (Array.isArray(text)) { + // size token 不能超过 8192 + const allSize = text.reduce((acc, item) => acc + numTokensFromString(item), 0); + if (allSize > 8192) { + throw new Error('text size 不能超过 8192'); + } + } + const res = await this.generateEmbeddingCore(text); + if (Array.isArray(text)) { + return res.data.map((item) => item.embedding); + } + return [res.data[0].embedding]; + } + /** + * 批量生成embedding + * @param text + * @returns + */ + async generateEmbeddingBatch(textArray: string[]) { + const batchSize = this.batchSize || 4; + const embeddings: number[][] = []; + for (let i = 0; i < textArray.length; i += batchSize) { + const batch = textArray.slice(i, i + batchSize); + const res = await this.generateEmbeddingBatchCore(batch); + embeddings.push(...res); + } + return embeddings; + } + /** + * 分割长文本, 生成对应的embedding + * @param text + * @returns + */ + async splitLongText(text: string) { + // 分割文本 + const chunks: string[] = []; + let startIndex = 0; + + while (startIndex < text.length) { + // 计算当前chunk的结束位置 + const endIndex = Math.min(startIndex + this.splitSize, text.length); + + // 提取当前chunk + const chunk = text.substring(startIndex, endIndex); + chunks.push(chunk); + + // 移动到下一个起始位置,考虑重叠 + startIndex = endIndex - this.splitOverlap; + + // 如果下一个起始位置已经超出或者太接近文本结尾,就结束循环 + if (startIndex >= text.length - this.splitOverlap) { + break; + } + } + + // 为每个chunk生成embedding + const embeddings = await this.generateEmbeddingBatch(chunks); + // 返回文本片段和对应的embedding + return chunks.map((chunk, index) => ({ + text: chunk, + embedding: embeddings[index], + })); + } +} diff --git a/src/provider/knowledge/knowledge.ts b/src/provider/knowledge/knowledge.ts new file mode 100644 index 0000000..1337510 --- /dev/null +++ b/src/provider/knowledge/knowledge.ts @@ -0,0 +1,7 @@ +import { KnowledgeBase, KnowledgeOptions } from './knowledge-base.ts'; + +export class Knowledge extends KnowledgeBase { + constructor(options: KnowledgeOptions) { + super(options); + } +} diff --git a/src/provider/utils/chunk.ts b/src/provider/utils/chunk.ts new file mode 100644 index 0000000..f6d3f88 --- /dev/null +++ b/src/provider/utils/chunk.ts @@ -0,0 +1,86 @@ +import { numTokensFromString } from './token.ts'; + +// 常量定义 +const CHUNK_SIZE = 512; // 每个chunk的最大token数 +const MAGIC_SEPARATOR = '🦛'; +const DELIMITER = [',', '.', '!', '?', '\n', ',', '。', '!', '?']; +const PARAGRAPH_DELIMITER = '\n\n'; + +export interface Chunk { + chunkId: number; + text: string; + tokens: number; +} + +/** + * 确保每个chunk的大小不超过最大token数 + * @param chunk 输入的文本块 + * @returns 分割后的文本块及其token数的数组 + */ +function ensureChunkSize(chunk: string): Array<[string, number]> { + const tokens = numTokensFromString(chunk); + if (tokens <= CHUNK_SIZE) { + return [[chunk, tokens]]; + } + + // 在分隔符后添加魔法分隔符 + let processedChunk = chunk; + for (const delimiter of DELIMITER) { + // 转义特殊字符 + const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + processedChunk = processedChunk.replace(new RegExp(escapedDelimiter, 'g'), delimiter + MAGIC_SEPARATOR); + } + + const chunks: Array<[string, number]> = []; + let tail = ''; + + // 按CHUNK_SIZE分割文本 + for (let i = 0; i < processedChunk.length; i += CHUNK_SIZE) { + const sentences = (processedChunk.slice(i, i + CHUNK_SIZE) + ' ').split(MAGIC_SEPARATOR); + const currentChunk = tail + sentences.slice(0, -1).join(''); + if (currentChunk.trim()) { + const tokenCount = numTokensFromString(currentChunk); + chunks.push([currentChunk, tokenCount]); + } + tail = sentences[sentences.length - 1].trim(); + } + + // 处理最后剩余的tail + if (tail) { + const tokenCount = numTokensFromString(tail); + chunks.push([tail, tokenCount]); + } + + return chunks; +} + +/** + * 将文本分割成chunks + * @param text 输入文本 + * @returns 分割后的chunks数组 + */ +export async function getChunks(text: string): Promise { + // 按段落分割文本 + const paragraphs = text + .split(PARAGRAPH_DELIMITER) + .map((p) => p.trim()) + .filter((p) => p); + + const chunks: Chunk[] = []; + let currentIndex = 0; + + // 处理每个段落 + for (const paragraph of paragraphs) { + const splittedParagraph = ensureChunkSize(paragraph); + for (const [text, tokens] of splittedParagraph) { + chunks.push({ + chunkId: currentIndex, + text, + tokens, + }); + currentIndex++; + } + } + + return chunks; +} diff --git a/src/provider/utils/parse-config.ts b/src/provider/utils/parse-config.ts new file mode 100644 index 0000000..d74bc1a --- /dev/null +++ b/src/provider/utils/parse-config.ts @@ -0,0 +1,141 @@ +import { AES, enc } from 'crypto-js'; + +// 加密函数 +export function encryptAES(plainText: string, secretKey: string) { + return AES.encrypt(plainText, secretKey).toString(); +} + +// 解密函数 +export function decryptAES(cipherText: string, secretKey: string) { + const bytes = AES.decrypt(cipherText, secretKey); + return bytes.toString(enc.Utf8); +} + +type AIModel = { + /** + * 提供商 + */ + provider: string; + /** + * 模型名称 + */ + model: string; + /** + * 模型组 + */ + group: string; + /** + * 每日限制 + */ + dayLimit?: number; +}; + +type SecretKey = { + /** + * 组 + */ + group: string; + /** + * API密钥 + */ + apiKey: string; + /** + * 解密密钥 + */ + decryptKey?: string; +}; + +export type GetProviderOpts = { + model: string; + group: string; + decryptKey?: string; +}; +export type ProviderResult = { + provider: string; + model: string; + group: string; + apiKey: string; + dayLimit?: number; + baseURL?: string; + /** + * 解密密钥 + */ + decryptKey?: string; +}; + +export type AIConfig = { + title?: string; + description?: string; + models: AIModel[]; + secretKeys: SecretKey[]; +}; +export class AIConfigParser { + private config: AIConfig; + result: ProviderResult; + constructor(config: AIConfig) { + this.config = config; + } + + getProvider(opts: GetProviderOpts): ProviderResult { + const { model, group, decryptKey } = opts; + const modelConfig = this.config.models.find((m) => m.model === model && m.group === group); + const groupConfig = this.config.secretKeys.find((m) => m.group === group); + if (!modelConfig) { + throw new Error(`在模型组 ${group} 中未找到模型 ${model}`); + } + const mergeConfig = { + ...modelConfig, + ...groupConfig, + decryptKey: decryptKey || groupConfig?.decryptKey, + }; + // 验证模型配置 + if (!mergeConfig.provider) { + throw new Error(`模型 ${model} 未配置提供商`); + } + if (!mergeConfig.model) { + throw new Error(`模型 ${model} 未配置模型名称`); + } + if (!mergeConfig.apiKey) { + throw new Error(`组 ${group} 未配置 API 密钥`); + } + if (!mergeConfig.group) { + throw new Error(`组 ${group} 未配置`); + } + this.result = mergeConfig; + return mergeConfig; + } + + async getSecretKey({ + getCache, + setCache, + providerResult, + }: { + getCache?: (key: string) => Promise; + setCache?: (key: string, value: string) => Promise; + providerResult?: ProviderResult; + }) { + const { apiKey, decryptKey, group = '', model } = providerResult || this.result; + const cacheKey = `${group}--${model}`; + if (!decryptKey) { + return apiKey; + } + if (getCache) { + const cache = await getCache(cacheKey); + if (cache) { + return cache; + } + } + const secretKey = decryptAES(apiKey, decryptKey); + if (setCache) { + await setCache(cacheKey, secretKey); + } + return secretKey; + } + encrypt(plainText: string, secretKey: string) { + return encryptAES(plainText, secretKey); + } + + decrypt(cipherText: string, secretKey: string) { + return decryptAES(cipherText, secretKey); + } +} diff --git a/src/provider/utils/token.ts b/src/provider/utils/token.ts new file mode 100644 index 0000000..81ce82c --- /dev/null +++ b/src/provider/utils/token.ts @@ -0,0 +1,34 @@ +import { encoding_for_model, get_encoding } from 'tiktoken'; + + +const MODEL_TO_ENCODING = { + 'gpt-4': 'cl100k_base', + 'gpt-4-turbo': 'cl100k_base', + 'gpt-3.5-turbo': 'cl100k_base', + 'text-embedding-ada-002': 'cl100k_base', + 'text-davinci-002': 'p50k_base', + 'text-davinci-003': 'p50k_base', +} as const; + +export function numTokensFromString(text: string, model: keyof typeof MODEL_TO_ENCODING = 'gpt-3.5-turbo'): number { + try { + // 对于特定模型使用专门的编码器 + const encoder = encoding_for_model(model); + const tokens = encoder.encode(text); + const tokenCount = tokens.length; + encoder.free(); // 释放编码器 + return tokenCount; + } catch (error) { + try { + // 如果模型特定的编码器失败,尝试使用基础编码器 + const encoder = get_encoding(MODEL_TO_ENCODING[model]); + const tokens = encoder.encode(text); + const tokenCount = tokens.length; + encoder.free(); // 释放编码器 + return tokenCount; + } catch (error) { + // 如果编码失败,使用一个粗略的估计:平均每个字符0.25个token + return Math.ceil(text.length * 0.25); + } + } +} diff --git a/src/routes/ai-chat/index.ts b/src/routes/ai-chat/index.ts new file mode 100644 index 0000000..2c891c2 --- /dev/null +++ b/src/routes/ai-chat/index.ts @@ -0,0 +1,11 @@ +import { app } from '@/app.ts'; + +app + .route({ + path: 'ai', + key: 'chat', + }) + .define(async () => { + // + }) + .addTo(app); diff --git a/src/routes/ai-chat/services/chat-services.ts b/src/routes/ai-chat/services/chat-services.ts new file mode 100644 index 0000000..d98292b --- /dev/null +++ b/src/routes/ai-chat/services/chat-services.ts @@ -0,0 +1,91 @@ +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 { redis } from '@/modules/db.ts'; +export type ChatServicesConfig = { + username: string; + model: string; + group: string; + decryptKey?: string; +}; +export class ChatServices { + cachePrefix = 'ai-chat:model:'; + /** + * 用户名 + */ + username: string; + /** + * 模型 + */ + model: string; + /** + * 组 + */ + group: string; + /** + * 解密密钥 + */ + decryptKey?: string; + /** + * 模型配置 + */ + modelConfig?: ProviderResult; + chatProvider?: BaseChat; + constructor(opts: ChatServicesConfig) { + this.username = opts.username; + this.model = opts.model; + this.group = opts.group; + this.decryptKey = opts.decryptKey; + } + /** + * 初始化 + * @returns + */ + async init() { + const config = await this.getConfig(); + const aiConfigParser = new AIConfigParser(config); + const model = this.model; + const group = this.group; + const decryptKey = this.decryptKey; + const providerResult = aiConfigParser.getProvider({ model, group, decryptKey }); + const that = this; + const apiKey = await aiConfigParser.getSecretKey({ + getCache: async (key) => { + const cache = await redis.get(that.wrapperKey(key)); + return cache; + }, + setCache: async (key, value) => { + await redis.set(that.wrapperKey(key), value); + }, + }); + that.modelConfig = { ...providerResult, apiKey }; + return that.modelConfig; + } + async wrapperKey(key: string) { + const username = this.username; + return `${this.cachePrefix}${username}:${key}`; + } + async getConfig() { + return getChatConfig(); + } + + async chat(messages: ChatMessage[]) { + const { model, provider, apiKey, baseURL } = this.modelConfig; + const providerManager = await ProviderManager.createProvider({ + provider: provider, + model: model, + apiKey: apiKey, + baseURL: baseURL, + }); + this.chatProvider = providerManager; + const result = await providerManager.chat(messages); + return result; + } + static async createServices(opts: Partial) { + const username = opts.username || 'root'; + const model = opts.model || 'deepseek-r1-250120'; + const group = opts.group || 'deepseek'; + const decryptKey = opts.decryptKey; + return new ChatServices({ username, model, group, decryptKey }); + } +} diff --git a/src/routes/ai-manager/models/provider.ts b/src/routes/ai-manager/models/provider.ts deleted file mode 100644 index 6bab763..0000000 --- a/src/routes/ai-manager/models/provider.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { sequelize } from '@kevisual/code-center-module'; -import { DataTypes, Model } from 'sequelize'; -export type Provider = Partial>; - -type ModelItem = { - /** - * 模型 - */ - model: string; - /** - * 提供者 - */ - provider: string; - /** - * 配置, 自有配置 - */ - config: Record; - /** - * 标题 - */ - title: string; - /** - * 描述 - */ - description: string; -}; -export type ProviderData = { - models: ModelItem[]; - config: Record; // 共享配置 -}; -export class ProviderModel extends Model { - declare id: string; - declare title: string; - declare description: string; - - declare config: Record; - declare data: Record; - - declare uid: string; - declare createdAt: Date; - declare updatedAt: Date; -} - -ProviderModel.init( - { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - comment: 'id', - }, - title: { - type: DataTypes.TEXT, - defaultValue: '', - }, - description: { - type: DataTypes.TEXT, - defaultValue: '', - }, - config: { - type: DataTypes.JSON, - defaultValue: {}, - }, - data: { - type: DataTypes.JSON, - defaultValue: {}, - }, - uid: { - type: DataTypes.UUID, - allowNull: true, - }, - }, - { - sequelize, - tableName: 'kv_provider', - paranoid: true, - }, -); diff --git a/src/test/chunks/01-get.ts b/src/test/chunks/01-get.ts new file mode 100644 index 0000000..65e0128 --- /dev/null +++ b/src/test/chunks/01-get.ts @@ -0,0 +1,65 @@ +import { getChunks } from '../../provider/utils/chunk.ts'; + +const str = 'Hello world this is a test 你好沙盒 very big'; + + +const str2 = `不能直接使用 tiktoken(OpenAI的分词器)来计算 Qwen 模型的 Token 数量,因为两者的分词规则(Tokenization)和词表(Vocabulary)完全不同。 + +为什么不能混用? +词表不同 + +tiktoken 是 OpenAI 为 GPT 系列设计的(如 gpt-3.5-turbo, gpt-4),其词表针对英语和代码优化。 + +Qwen 使用独立训练的 BPE 词表,对中文、多语言的支持更友好,分词粒度可能不同。 + +分词结果差异大 +同一段文本,tiktoken 和 Qwen 的分词结果可能完全不同。例如: + +OpenAI (tiktoken): "你好" → ['你', '好'](2 Tokens) + +Qwen: "你好" → ['你好'](1 Token,如果词表中包含该组合) + +性能问题 +即使强制使用 tiktoken 计算 Qwen 的 Token,结果也不准确,可能导致: + +输入超出模型上下文限制(因统计偏差)。 + +API 计费或本地推理时出现意外错误。 + +正确方法:用 Qwen 的分词器 +通过 Hugging Face transformers 加载 Qwen 的原生分词器: + +python +复制 +from transformers import AutoTokenizer + +# 加载 Qwen 的分词器(以 Qwen-7B 为例) +tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen-7B", trust_remote_code=True) + +text = "你好,Qwen模型!" +tokens = tokenizer.tokenize(text) # 查看分词结果 +token_count = len(tokenizer.encode(text, add_special_tokens=False)) + +print("分词结果:", tokens) +print("Token数量:", token_count) +常见问题 +为什么需要 trust_remote_code=True? +Qwen 的分词器是自定义实现的(非 Hugging Face 原生),此参数允许从模型仓库加载运行代码。 + +其他语言的 Token 计算? +Qwen 对非英语(如中文、日文)的分词效率较高,但仍需用其原生分词器统计。 + +与 tiktoken 的速度对比? +tiktoken 是纯 Python 实现,速度较快;Qwen 的分词器基于 Hugging Face,可能稍慢但对齐模型需求。 + +总结 +禁止混用:tiktoken ≠ Qwen 分词器。 + +始终使用模型配套工具:Qwen 需通过 transformers 加载其官方分词器。 + +中文场景特别注意:Qwen 对中文的分词更高效,直接使用可避免偏差。 + +如果需要验证分词规则,可通过 tokenizer.vocab 查看词表内容(但注意词表通常较大)。` + +const chunks = getChunks(str2); +console.log(chunks); diff --git a/src/test/ollama-knowledge.ts b/src/test/ollama-knowledge.ts new file mode 100644 index 0000000..3dc0553 --- /dev/null +++ b/src/test/ollama-knowledge.ts @@ -0,0 +1,37 @@ +import { Knowledge } from '../provider/knowledge/knowledge.ts'; +import fs from 'fs'; +import dotenv from 'dotenv'; + +dotenv.config(); +const knowledge = new Knowledge({ + embeddingModel: 'bge-m3:latest', + baseURL: 'https://ollama.xiongxiao.me/v1', + model: 'qwq:latest', + apiKey: process.env.OLLAMA_API_KEY, +}); + +const main = async () => { + const res = await knowledge.generateEmbeddingCore('Hello world this is a test 你好沙盒 very big'); + fs.writeFileSync('docs/embedding.json', JSON.stringify(res, null, 2)); + console.log(res); +}; + +main(); + +const main2 = async () => { + const text1 = 'Hello, world! this is a test'; + const text2 = 'Hello, world! this is a test 2'; + const text3 = 'Hello, world! this is a test 3'; + const text4 = 'Hello, world! this is a test 4'; + const text5 = 'Hello, world! this is a test 5'; + const text6 = 'Hello, world! this is a test 6'; + const text7 = 'Hello, world! this is a test 7'; + const text8 = 'Hello, world! this is a test 8'; + const text9 = 'Hello, world! this is a test 9'; + const text10 = 'Hello, world! this is a test 10'; + const res = await knowledge.generateEmbeddingCore([text1, text2, text3, text4, text5, text6, text7, text8, text9, text10]); + fs.writeFileSync('docs/embedding2.json', JSON.stringify(res, null, 2)); + console.log(res); +}; + +// main2(); diff --git a/src/test/ollama.ts b/src/test/ollama.ts new file mode 100644 index 0000000..f47b1cf --- /dev/null +++ b/src/test/ollama.ts @@ -0,0 +1,86 @@ +import { Ollama } from '../provider/chat-adapter/ollama.ts'; +import util from 'util'; +const chat = new Ollama({ + baseURL: 'https://ollama.xiongxiao.me/v1', + apiKey: 'xiongxiao2233', + model: 'qwq:latest', +}); + +// chat.chat([{ role: 'user', content: 'Hello, world!' }]); + +const main = async () => { + const res = await chat.test(); + console.log(util.inspect(res, { depth: null, colors: true })); +}; + +// main(); + +const getJson = async () => { + const res = await chat.chat( + [ + { role: 'system', content: '把发送的数据,返回给我对应的json,只处理完发送的数据。如果发送了多个,给我一个数组' }, + // { role: 'user', content: '{"name":"John","age":30}' }, + { role: 'user', content: 'name: 张三' }, + { role: 'user', content: 'name: 李四, age: 18' }, + ], + { + response_format: { + type: 'json_schema', + json_schema: { + name: 'user', + description: '用户信息', + schema: { + type: 'object', + // properties: { + // name: { type: 'string' }, + // // age: { type: 'number' }, + // }, + // // required: ['name', 'age'], + // required: ['name'], + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + required: ['name', 'age'], + }, + }, + }, + n: 10, + }, + ); + console.log(util.inspect(res, { depth: null, colors: true })); +}; + +// getJson(); + +const createChat1 = async () => { + const res = await chat.chat( + [ + { role: 'user', content: 'a=1, b=2, c=3' }, + { role: 'user', content: 'a+b+c=?' }, + { role: 'assistant', content: '给定的值为 \\( a = 1 \\), \\( b = 2 \\), \\( c = 3 \\)。\n' + '\n' + '因此,\\( a + b + c = 1 + 2 + 3 = 6 \\)。' }, + { role: 'user', content: 'a+b+c+4=?' }, + ], + { + model: 'qwen2.5:7b', + }, + ); + console.log(util.inspect(res, { depth: null, colors: true })); +}; + +// createChat1(); + +const getTags = async () => { + const res = await chat.listModels(); + console.log(util.inspect(res, { depth: null, colors: true })); +}; + +// getTags(); + +const getRunModels = async () => { + const res = await chat.listRunModels(); + console.log('current', new Date().toISOString()); + console.log(util.inspect(res, { depth: null, colors: true })); +}; + +// getRunModels(); diff --git a/src/test/siliconflow/get.ts b/src/test/siliconflow/get.ts new file mode 100644 index 0000000..54e529c --- /dev/null +++ b/src/test/siliconflow/get.ts @@ -0,0 +1,15 @@ +import { SiliconFlow } from '../../provider/chat-adapter/siliconflow.ts'; +import dotenv from 'dotenv'; + +dotenv.config(); +const siliconflow = new SiliconFlow({ + apiKey: process.env.SILICONFLOW_API_KEY, + model: 'Qwen/Qwen2-7B-Instruct', +}); + +const main = async () => { + const usage = await siliconflow.getUsage(); + console.log(usage); +}; + +main(); diff --git a/vite.config.mjs b/vite.config.mjs new file mode 100644 index 0000000..e2cfd44 --- /dev/null +++ b/vite.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import basicSsl from '@vitejs/plugin-basic-ssl'; + +export default defineConfig({ + plugins: [basicSsl()], + server: { + port: 3000, + }, +});