@@ -39,6 +39,7 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.7.5",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csv": "^6.3.11",
|
"csv": "^6.3.11",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"multer-s3": "^3.0.1",
|
"multer-s3": "^3.0.1",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
|
"openai": "^4.0.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
@@ -66,6 +68,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"strip-ansi": "6.0.1",
|
"strip-ansi": "6.0.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"ulid": "^3.0.1",
|
||||||
"winston": "^3.17.0"
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
"@commitlint/config-conventional": "^19.5.0",
|
"@commitlint/config-conventional": "^19.5.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/cookie-parser": "^1.4.10",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
|||||||
Generated
+244
@@ -38,6 +38,9 @@ importers:
|
|||||||
compression:
|
compression:
|
||||||
specifier: ^1.7.5
|
specifier: ^1.7.5
|
||||||
version: 1.7.5
|
version: 1.7.5
|
||||||
|
cookie-parser:
|
||||||
|
specifier: ^1.4.7
|
||||||
|
version: 1.4.7
|
||||||
cors:
|
cors:
|
||||||
specifier: ^2.8.5
|
specifier: ^2.8.5
|
||||||
version: 2.8.5
|
version: 2.8.5
|
||||||
@@ -98,6 +101,9 @@ importers:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^6.9.16
|
specifier: ^6.9.16
|
||||||
version: 6.9.16
|
version: 6.9.16
|
||||||
|
openai:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.104.0(zod@3.25.76)
|
||||||
passport:
|
passport:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
@@ -119,6 +125,9 @@ importers:
|
|||||||
swagger-ui-express:
|
swagger-ui-express:
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1(express@4.21.1)
|
version: 5.0.1(express@4.21.1)
|
||||||
|
ulid:
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1
|
||||||
winston:
|
winston:
|
||||||
specifier: ^3.17.0
|
specifier: ^3.17.0
|
||||||
version: 3.17.0
|
version: 3.17.0
|
||||||
@@ -135,6 +144,9 @@ importers:
|
|||||||
'@types/compression':
|
'@types/compression':
|
||||||
specifier: ^1.7.5
|
specifier: ^1.7.5
|
||||||
version: 1.7.5
|
version: 1.7.5
|
||||||
|
'@types/cookie-parser':
|
||||||
|
specifier: ^1.4.10
|
||||||
|
version: 1.4.10(@types/express@4.17.21)
|
||||||
'@types/cors':
|
'@types/cors':
|
||||||
specifier: ^2.8.17
|
specifier: ^2.8.17
|
||||||
version: 2.8.17
|
version: 2.8.17
|
||||||
@@ -1014,6 +1026,11 @@ packages:
|
|||||||
'@types/conventional-commits-parser@5.0.0':
|
'@types/conventional-commits-parser@5.0.0':
|
||||||
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
|
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.10':
|
||||||
|
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/express': '*'
|
||||||
|
|
||||||
'@types/cookie@0.4.1':
|
'@types/cookie@0.4.1':
|
||||||
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
||||||
|
|
||||||
@@ -1050,6 +1067,12 @@ packages:
|
|||||||
'@types/multer@1.4.12':
|
'@types/multer@1.4.12':
|
||||||
resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==}
|
resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==}
|
||||||
|
|
||||||
|
'@types/node-fetch@2.6.13':
|
||||||
|
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||||
|
|
||||||
|
'@types/node@18.19.130':
|
||||||
|
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||||
|
|
||||||
'@types/node@20.17.6':
|
'@types/node@20.17.6':
|
||||||
resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==}
|
resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==}
|
||||||
|
|
||||||
@@ -1162,6 +1185,10 @@ packages:
|
|||||||
abbrev@1.1.1:
|
abbrev@1.1.1:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -1184,6 +1211,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
|
agentkeepalive@4.6.0:
|
||||||
|
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||||
|
engines: {node: '>= 8.0.0'}
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||||
|
|
||||||
@@ -1355,6 +1386,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
call-bind@1.0.7:
|
call-bind@1.0.7:
|
||||||
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
|
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1507,6 +1542,10 @@ packages:
|
|||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
cookie-parser@1.4.7:
|
||||||
|
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
@@ -1667,6 +1706,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
@@ -1723,6 +1766,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
|
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-define-property@1.0.1:
|
||||||
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
es-errors@1.3.0:
|
es-errors@1.3.0:
|
||||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1731,10 +1778,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
es-set-tostringtag@2.0.3:
|
es-set-tostringtag@2.0.3:
|
||||||
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
|
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-set-tostringtag@2.1.0:
|
||||||
|
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
es-shim-unscopables@1.0.2:
|
es-shim-unscopables@1.0.2:
|
||||||
resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
|
resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
|
||||||
|
|
||||||
@@ -1854,6 +1909,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
eventemitter3@5.0.1:
|
eventemitter3@5.0.1:
|
||||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||||
|
|
||||||
@@ -1965,10 +2024,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
form-data-encoder@1.7.2:
|
||||||
|
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||||
|
|
||||||
form-data@4.0.1:
|
form-data@4.0.1:
|
||||||
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
|
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
formdata-node@4.4.1:
|
||||||
|
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||||
|
engines: {node: '>= 12.20'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -2016,6 +2086,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
get-stream@8.0.1:
|
get-stream@8.0.1:
|
||||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -2064,6 +2142,10 @@ packages:
|
|||||||
gopd@1.0.1:
|
gopd@1.0.1:
|
||||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
|
|
||||||
|
gopd@1.2.0:
|
||||||
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
graphemer@1.4.0:
|
graphemer@1.4.0:
|
||||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||||
|
|
||||||
@@ -2094,6 +2176,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
has-symbols@1.1.0:
|
||||||
|
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
has-tostringtag@1.0.2:
|
has-tostringtag@1.0.2:
|
||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2123,6 +2209,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||||
engines: {node: '>=16.17.0'}
|
engines: {node: '>=16.17.0'}
|
||||||
|
|
||||||
|
humanize-ms@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||||
|
|
||||||
husky@9.1.6:
|
husky@9.1.6:
|
||||||
resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==}
|
resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2504,6 +2593,10 @@ packages:
|
|||||||
make-error@1.3.6:
|
make-error@1.3.6:
|
||||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -2720,6 +2813,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
|
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
|
|
||||||
|
node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -2817,6 +2915,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
openai@4.104.0:
|
||||||
|
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -3413,12 +3523,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ulid@3.0.1:
|
||||||
|
resolution: {integrity: sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
unbox-primitive@1.0.2:
|
unbox-primitive@1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
|
|
||||||
undefsafe@2.0.5:
|
undefsafe@2.0.5:
|
||||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
|
undici-types@5.26.5:
|
||||||
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
undici-types@6.19.8:
|
undici-types@6.19.8:
|
||||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||||
|
|
||||||
@@ -3458,6 +3575,10 @@ packages:
|
|||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||||
|
|
||||||
|
web-streams-polyfill@4.0.0-beta.3:
|
||||||
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
@@ -3573,6 +3694,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
|
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@aws-crypto/crc32@5.2.0':
|
'@aws-crypto/crc32@5.2.0':
|
||||||
@@ -4885,6 +5009,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.6
|
'@types/node': 20.17.6
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.10(@types/express@4.17.21)':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
|
||||||
'@types/cookie@0.4.1': {}
|
'@types/cookie@0.4.1': {}
|
||||||
|
|
||||||
'@types/cors@2.8.17':
|
'@types/cors@2.8.17':
|
||||||
@@ -4940,6 +5068,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/express': 4.17.21
|
'@types/express': 4.17.21
|
||||||
|
|
||||||
|
'@types/node-fetch@2.6.13':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.17.6
|
||||||
|
form-data: 4.0.5
|
||||||
|
|
||||||
|
'@types/node@18.19.130':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 5.26.5
|
||||||
|
|
||||||
'@types/node@20.17.6':
|
'@types/node@20.17.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.19.8
|
undici-types: 6.19.8
|
||||||
@@ -5087,6 +5224,10 @@ snapshots:
|
|||||||
|
|
||||||
abbrev@1.1.1: {}
|
abbrev@1.1.1: {}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -5108,6 +5249,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
agentkeepalive@4.6.0:
|
||||||
|
dependencies:
|
||||||
|
humanize-ms: 1.2.1
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@@ -5324,6 +5469,11 @@ snapshots:
|
|||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
call-bind@1.0.7:
|
call-bind@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.0
|
es-define-property: 1.0.0
|
||||||
@@ -5486,6 +5636,11 @@ snapshots:
|
|||||||
meow: 12.1.1
|
meow: 12.1.1
|
||||||
split2: 4.2.0
|
split2: 4.2.0
|
||||||
|
|
||||||
|
cookie-parser@1.4.7:
|
||||||
|
dependencies:
|
||||||
|
cookie: 0.7.2
|
||||||
|
cookie-signature: 1.0.6
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
cookie@0.7.1: {}
|
cookie@0.7.1: {}
|
||||||
@@ -5622,6 +5777,12 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.4.5: {}
|
dotenv@16.4.5: {}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-errors: 1.3.0
|
||||||
|
gopd: 1.2.0
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
ecdsa-sig-formatter@1.0.11:
|
||||||
@@ -5722,18 +5883,31 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
|
|
||||||
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
|
|
||||||
es-object-atoms@1.0.0:
|
es-object-atoms@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
|
||||||
es-set-tostringtag@2.0.3:
|
es-set-tostringtag@2.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
|
|
||||||
|
es-set-tostringtag@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
hasown: 2.0.2
|
||||||
|
|
||||||
es-shim-unscopables@1.0.2:
|
es-shim-unscopables@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
@@ -5909,6 +6083,8 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
eventemitter3@5.0.1: {}
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
@@ -6061,12 +6237,27 @@ snapshots:
|
|||||||
cross-spawn: 7.0.5
|
cross-spawn: 7.0.5
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
form-data-encoder@1.7.2: {}
|
||||||
|
|
||||||
form-data@4.0.1:
|
form-data@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
form-data@4.0.5:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
formdata-node@4.4.1:
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 4.0.0-beta.3
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
@@ -6115,6 +6306,24 @@ snapshots:
|
|||||||
has-symbols: 1.0.3
|
has-symbols: 1.0.3
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-proto: 1.0.1
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-symbols: 1.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
dunder-proto: 1.0.1
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
get-stream@8.0.1: {}
|
get-stream@8.0.1: {}
|
||||||
|
|
||||||
get-symbol-description@1.0.2:
|
get-symbol-description@1.0.2:
|
||||||
@@ -6176,6 +6385,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
|
|
||||||
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
graphemer@1.4.0: {}
|
graphemer@1.4.0: {}
|
||||||
|
|
||||||
handlebars@4.7.8:
|
handlebars@4.7.8:
|
||||||
@@ -6201,6 +6412,8 @@ snapshots:
|
|||||||
|
|
||||||
has-symbols@1.0.3: {}
|
has-symbols@1.0.3: {}
|
||||||
|
|
||||||
|
has-symbols@1.1.0: {}
|
||||||
|
|
||||||
has-tostringtag@1.0.2:
|
has-tostringtag@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.0.3
|
has-symbols: 1.0.3
|
||||||
@@ -6232,6 +6445,10 @@ snapshots:
|
|||||||
|
|
||||||
human-signals@5.0.0: {}
|
human-signals@5.0.0: {}
|
||||||
|
|
||||||
|
humanize-ms@1.2.1:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
|
||||||
husky@9.1.6: {}
|
husky@9.1.6: {}
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
@@ -6611,6 +6828,8 @@ snapshots:
|
|||||||
|
|
||||||
make-error@1.3.6: {}
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
memory-pager@1.5.0: {}
|
memory-pager@1.5.0: {}
|
||||||
@@ -6831,6 +7050,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clone: 2.1.2
|
clone: 2.1.2
|
||||||
|
|
||||||
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
@@ -6934,6 +7155,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-function: 5.0.1
|
mimic-function: 5.0.1
|
||||||
|
|
||||||
|
openai@4.104.0(zod@3.25.76):
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.130
|
||||||
|
'@types/node-fetch': 2.6.13
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
agentkeepalive: 4.6.0
|
||||||
|
form-data-encoder: 1.7.2
|
||||||
|
formdata-node: 4.4.1
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 3.25.76
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@@ -7551,6 +7786,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@lukeed/csprng': 1.1.0
|
'@lukeed/csprng': 1.1.0
|
||||||
|
|
||||||
|
ulid@3.0.1: {}
|
||||||
|
|
||||||
unbox-primitive@1.0.2:
|
unbox-primitive@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
@@ -7560,6 +7797,8 @@ snapshots:
|
|||||||
|
|
||||||
undefsafe@2.0.5: {}
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
|
undici-types@5.26.5: {}
|
||||||
|
|
||||||
undici-types@6.19.8: {}
|
undici-types@6.19.8: {}
|
||||||
|
|
||||||
unicorn-magic@0.1.0: {}
|
unicorn-magic@0.1.0: {}
|
||||||
@@ -7586,6 +7825,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
|
|
||||||
|
web-streams-polyfill@4.0.0-beta.3: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
webidl-conversions@7.0.0: {}
|
||||||
@@ -7706,3 +7947,6 @@ snapshots:
|
|||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
yocto-queue@1.1.1: {}
|
yocto-queue@1.1.1: {}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
optional: true
|
||||||
|
|||||||
+10
-1
@@ -54,6 +54,11 @@ import { ChatService } from "../modules/chat/chat.service";
|
|||||||
import { ChatRepo, createChatRepo } from "../modules/chat/repository/chat.repository";
|
import { ChatRepo, createChatRepo } from "../modules/chat/repository/chat.repository";
|
||||||
import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/repository/message.repository";
|
import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/repository/message.repository";
|
||||||
import { WsAuthService } from "../modules/chat/wsAuth.service";
|
import { WsAuthService } from "../modules/chat/wsAuth.service";
|
||||||
|
import { ChatbotGateway } from "../modules/chatbot/chatbot.gateway";
|
||||||
|
import { ChatbotService } from "../modules/chatbot/providers/chatbot.service";
|
||||||
|
import { DataContextService } from "../modules/chatbot/providers/data-context.service";
|
||||||
|
import { LLMService } from "../modules/chatbot/providers/llm.service";
|
||||||
|
import { WebSocketAuthService } from "../modules/chatbot/providers/websocket-auth.service";
|
||||||
import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
|
import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
|
||||||
import { ContactUsService } from "../modules/contact-us/contactUs.service";
|
import { ContactUsService } from "../modules/contact-us/contactUs.service";
|
||||||
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository";
|
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository";
|
||||||
@@ -197,6 +202,11 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
|||||||
bind<LearningService>(IOCTYPES.LearningService).to(LearningService).inSingletonScope();
|
bind<LearningService>(IOCTYPES.LearningService).to(LearningService).inSingletonScope();
|
||||||
bind<RedisService>(IOCTYPES.RedisService).to(RedisService).inSingletonScope();
|
bind<RedisService>(IOCTYPES.RedisService).to(RedisService).inSingletonScope();
|
||||||
bind<CacheService>(IOCTYPES.CacheService).to(CacheService).inSingletonScope();
|
bind<CacheService>(IOCTYPES.CacheService).to(CacheService).inSingletonScope();
|
||||||
|
bind<ChatbotService>(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope();
|
||||||
|
bind<LLMService>(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope();
|
||||||
|
bind<DataContextService>(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope();
|
||||||
|
bind<WebSocketAuthService>(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope();
|
||||||
|
bind<ChatbotGateway>(IOCTYPES.ChatbotGateway).to(ChatbotGateway).inSingletonScope();
|
||||||
// #endregion
|
// #endregion
|
||||||
// #region repository
|
// #region repository
|
||||||
bind<CategoryRepository>(IOCTYPES.CategoryRepository).toDynamicValue(createCategoryRepo).inSingletonScope();
|
bind<CategoryRepository>(IOCTYPES.CategoryRepository).toDynamicValue(createCategoryRepo).inSingletonScope();
|
||||||
@@ -279,7 +289,6 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
|||||||
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
|
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
|
||||||
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
|
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
|
||||||
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
|
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
|
||||||
// #endregion
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export { containerModules };
|
export { containerModules };
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ export const IOCTYPES = {
|
|||||||
NewsletterService: Symbol.for("NewsletterService"),
|
NewsletterService: Symbol.for("NewsletterService"),
|
||||||
PricingService: Symbol.for("PricingService"),
|
PricingService: Symbol.for("PricingService"),
|
||||||
OrderQueue: Symbol.for("OrderQueue"),
|
OrderQueue: Symbol.for("OrderQueue"),
|
||||||
|
ChatbotService: Symbol.for("ChatbotService"),
|
||||||
|
ChatbotLLMService: Symbol.for("ChatbotLLMService"),
|
||||||
|
ChatbotDataContextService: Symbol.for("ChatbotDataContextService"),
|
||||||
|
ChatbotGateway: Symbol.for("ChatbotGateway"),
|
||||||
|
ChatbotWebSocketAuthService: Symbol.for("ChatbotWebSocketAuthService"),
|
||||||
// #endregion
|
// #endregion
|
||||||
// #region repository
|
// #region repository
|
||||||
CategoryRepository: Symbol.for("CategoryRepository"),
|
CategoryRepository: Symbol.for("CategoryRepository"),
|
||||||
@@ -129,6 +134,8 @@ export const IOCTYPES = {
|
|||||||
ContactUsRepo: Symbol.for("ContactUsRepo"),
|
ContactUsRepo: Symbol.for("ContactUsRepo"),
|
||||||
AboutUsRepo: Symbol.for("AboutUsRepo"),
|
AboutUsRepo: Symbol.for("AboutUsRepo"),
|
||||||
NewsletterRepo: Symbol.for("NewsletterRepo"),
|
NewsletterRepo: Symbol.for("NewsletterRepo"),
|
||||||
|
ChatbotChatSessionRepository: Symbol.for("ChatbotChatSessionRepository"),
|
||||||
|
ChatbotChatMessageRepository: Symbol.for("ChatbotChatMessageRepository"),
|
||||||
// #endregion
|
// #endregion
|
||||||
Logger: Symbol.for("Logger"),
|
Logger: Symbol.for("Logger"),
|
||||||
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
|
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
|
||||||
|
|||||||
+14
@@ -1,4 +1,7 @@
|
|||||||
|
// import path from "path";
|
||||||
|
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express, { Application } from "express";
|
import express, { Application } from "express";
|
||||||
import { Container } from "inversify";
|
import { Container } from "inversify";
|
||||||
@@ -31,6 +34,7 @@ import "./modules/blog/blog.controller";
|
|||||||
import "./modules/landing/landing.controller";
|
import "./modules/landing/landing.controller";
|
||||||
import "./modules/ticket/ticket.controller";
|
import "./modules/ticket/ticket.controller";
|
||||||
import "./modules/chat/chat.controller";
|
import "./modules/chat/chat.controller";
|
||||||
|
import "./modules/chatbot/chatbot.controller";
|
||||||
import "./modules/job/job.controller";
|
import "./modules/job/job.controller";
|
||||||
import "./modules/faq/faq.controller";
|
import "./modules/faq/faq.controller";
|
||||||
import "./modules/notification/notification.controller";
|
import "./modules/notification/notification.controller";
|
||||||
@@ -120,6 +124,11 @@ class App {
|
|||||||
*/
|
*/
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* configure cookie parser
|
||||||
|
*/
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* configure body parser
|
* configure body parser
|
||||||
*/
|
*/
|
||||||
@@ -135,6 +144,11 @@ class App {
|
|||||||
*/
|
*/
|
||||||
app.use(cors(appConfig.cors));
|
app.use(cors(appConfig.cors));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* serve static files from project root (for test files)
|
||||||
|
*/
|
||||||
|
app.use(express.static(process.cwd()));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* config passport
|
* config passport
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -329,3 +329,17 @@ export const enum RoleMessage {
|
|||||||
PermissionsNotEmpty = "دسترسیها نباید خالی باشد",
|
PermissionsNotEmpty = "دسترسیها نباید خالی باشد",
|
||||||
RoleExist = "این نقش قبلا ثبت شده است",
|
RoleExist = "این نقش قبلا ثبت شده است",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum WebSocketMessage {
|
||||||
|
AUTHENTICATED = "احراز هویت موفقیتآمیز",
|
||||||
|
AUTHENTICATION_REQUIRED = "احراز هویت ضروری است",
|
||||||
|
INVALID_TOKEN = "توکن احراز هویت نامعتبر است",
|
||||||
|
TOKEN_EXPIRED = "توکن منقضی شده است",
|
||||||
|
USER_NOT_AUTHENTICATED = "کاربر احراز هویت نشده است",
|
||||||
|
CONNECTION_FAILED = "اتصال برقرار نشد",
|
||||||
|
SESSION_CREATED = "جلسه چت با موفقیت ایجاد شد",
|
||||||
|
SESSION_CREATION_FAILED = "ایجاد جلسه چت با شکست مواجه شد",
|
||||||
|
SESSION_NOT_FOUND = "جلسه چت یافت نشد",
|
||||||
|
CHAT_JOINED = "با موفقیت به چت متصل شدید",
|
||||||
|
LLM_SERVICE_ERROR = "سرویس هوش مصنوعی موقتاً در دسترس نیست",
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
|
||||||
|
|
||||||
|
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||||
|
|
||||||
|
export class SendMessageDto {
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000)
|
||||||
|
@ApiProperty({ type: "string", description: "Message content", example: "How can I cancel my subscription?" })
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: "string", description: "Chat session ID (ULID)" })
|
||||||
|
sessionId!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: "string", description: "ID of message being replied to", required: false })
|
||||||
|
responseToId?: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ type: "object", description: "Additional metadata for the message", required: false })
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||||
|
|
||||||
|
export class SessionIdParamDto {
|
||||||
|
@Expose()
|
||||||
|
@IsNotEmpty({ message: "Session ID is required" })
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ type: "string", description: "Session id (ULID)", example: "01ARZ3NDEKTSV4RRFFQ69G5FAV" })
|
||||||
|
sessionId!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
||||||
|
|
||||||
|
export class AuthenticateDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
token!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateSessionDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
title!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JoinChatDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
sessionId!: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
userId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LeaveChatDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
sessionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendMessageWebSocketDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
sessionId!: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(4000)
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
responseToId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TypingDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
sessionId!: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
userId!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { inject } from "inversify";
|
||||||
|
import { controller, httpGet, httpPost, httpPut, queryParam, request, requestBody, requestParam, response } from "inversify-express-utils";
|
||||||
|
|
||||||
|
import { SendMessageDto } from "./DTO/send-message.dto";
|
||||||
|
import { SessionIdParamDto } from "./DTO/session-id.param.dto";
|
||||||
|
import { ChatbotService } from "./providers/chatbot.service";
|
||||||
|
import { getOrCreateChatbotUlid } from "./utils/ulid.util";
|
||||||
|
import { BaseController } from "../../common/base/controller";
|
||||||
|
import { ApiModel, ApiOperation, ApiParam, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
|
||||||
|
import { PaginationDTO } from "../../common/dto/pagination.dto";
|
||||||
|
import { HttpStatus } from "../../common/enums/httpStatus.enum";
|
||||||
|
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
|
||||||
|
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||||
|
|
||||||
|
@controller("/chatbot")
|
||||||
|
@ApiTags("Chatbot")
|
||||||
|
class ChatbotController extends BaseController {
|
||||||
|
@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService;
|
||||||
|
|
||||||
|
@ApiOperation("Create a new chat session")
|
||||||
|
@ApiResponse("Chat session created successfully", HttpStatus.Created)
|
||||||
|
@httpPost("/sessions")
|
||||||
|
public async createSession(@request() req: Request, @response() res: Response) {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
const data = await this.chatbotService.createChatSession(ulid);
|
||||||
|
return this.response({ data }, HttpStatus.Created);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("Get user's chat sessions")
|
||||||
|
@ApiResponse("Chat sessions retrieved successfully")
|
||||||
|
@httpGet("/sessions", ValidationMiddleware.validateQuery(PaginationDTO))
|
||||||
|
public async getUserSessions(@request() req: Request, @response() res: Response, @queryParam() queryDto: PaginationDTO) {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
const limit = queryDto.limit || 10;
|
||||||
|
const data = await this.chatbotService.getUserChatSessions(ulid, limit);
|
||||||
|
return this.response({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("Get a specific chat session with messages")
|
||||||
|
@ApiResponse("Chat session retrieved successfully")
|
||||||
|
@ApiParam("sessionId", "Session ID", true)
|
||||||
|
@httpGet("/sessions/:sessionId", ValidationMiddleware.validateParameter(SessionIdParamDto))
|
||||||
|
public async getSession(@request() req: Request, @response() res: Response, @requestParam() param: SessionIdParamDto) {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
const data = await this.chatbotService.getChatSession(param.sessionId, ulid);
|
||||||
|
return this.response({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("Send a message in a chat session")
|
||||||
|
@ApiResponse("Message sent successfully", HttpStatus.Created)
|
||||||
|
@ApiModel(SendMessageDto)
|
||||||
|
@httpPost("/messages", ValidationMiddleware.validateInput(SendMessageDto))
|
||||||
|
public async sendMessage(@request() req: Request, @response() res: Response, @requestBody() sendDto: SendMessageDto) {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
const data = await this.chatbotService.sendMessage(ulid, sendDto);
|
||||||
|
return this.response({ data }, HttpStatus.Created);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("Send a message and get streaming response")
|
||||||
|
@ApiResponse("Streaming response initiated")
|
||||||
|
@ApiModel(SendMessageDto)
|
||||||
|
@httpPost("/messages/stream", ValidationMiddleware.validateInput(SendMessageDto))
|
||||||
|
public async sendMessageStream(
|
||||||
|
@request() req: Request,
|
||||||
|
@requestBody() sendDto: SendMessageDto,
|
||||||
|
@response() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
try {
|
||||||
|
const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStream(ulid, sendDto);
|
||||||
|
|
||||||
|
// Set headers for Server-Sent Events
|
||||||
|
res.setHeader("Content-Type", "text/event-stream");
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
res.setHeader("Connection", "keep-alive");
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
|
// Send initial user message
|
||||||
|
res.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage })}\n\n`);
|
||||||
|
|
||||||
|
// Start streaming bot response
|
||||||
|
res.write(`data: ${JSON.stringify({ type: "bot_response_start" })}\n\n`);
|
||||||
|
|
||||||
|
let fullResponse = "";
|
||||||
|
const stream = await streamGenerator();
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk) {
|
||||||
|
fullResponse += chunk;
|
||||||
|
res.write(`data: ${JSON.stringify({ type: "bot_response_chunk", data: chunk })}\n\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save bot response to cache after streaming completes
|
||||||
|
if (fullResponse) {
|
||||||
|
await this.chatbotService.saveStreamedBotResponse(sendDto.sessionId, userMessage.id, fullResponse, ulid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// End streaming
|
||||||
|
res.write(`data: ${JSON.stringify({ type: "bot_response_end" })}\n\n`);
|
||||||
|
res.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.write(`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("Close a chat session")
|
||||||
|
@ApiResponse("Chat session closed successfully")
|
||||||
|
@ApiParam("sessionId", "Session ID", true)
|
||||||
|
@httpPut("/sessions/:sessionId/close", ValidationMiddleware.validateParameter(SessionIdParamDto))
|
||||||
|
public async closeSession(@request() req: Request, @response() res: Response, @requestParam() param: SessionIdParamDto) {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
const data = await this.chatbotService.closeChatSession(param.sessionId, ulid);
|
||||||
|
return this.response({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("Mark messages as read")
|
||||||
|
@ApiResponse("Messages marked as read successfully")
|
||||||
|
@ApiParam("sessionId", "Session ID", true)
|
||||||
|
@httpPut("/sessions/:sessionId/messages/read", ValidationMiddleware.validateParameter(SessionIdParamDto))
|
||||||
|
public async markMessagesAsRead(
|
||||||
|
@request() req: Request,
|
||||||
|
@response() res: Response,
|
||||||
|
@requestParam() param: SessionIdParamDto,
|
||||||
|
@requestBody() body: { messageIds: string[] },
|
||||||
|
) {
|
||||||
|
const ulid = getOrCreateChatbotUlid(req, res);
|
||||||
|
const data = await this.chatbotService.markMessagesAsRead(param.sessionId, ulid, body.messageIds);
|
||||||
|
return this.response({ data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChatbotController };
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { Server as HttpServer } from "http";
|
||||||
|
|
||||||
|
import { inject, injectable } from "inversify";
|
||||||
|
import { Server, Socket } from "socket.io";
|
||||||
|
|
||||||
|
import { ChatbotService } from "./providers/chatbot.service";
|
||||||
|
import { WebSocketAuthService } from "./providers/websocket-auth.service";
|
||||||
|
import { SendMessageDto } from "./DTO/send-message.dto";
|
||||||
|
import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants";
|
||||||
|
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||||
|
import { Logger } from "../../core/logging/logger";
|
||||||
|
import { AuthenticatedSocket, WebSocketResponse } from "./interfaces/websocket.interface";
|
||||||
|
import * as ulidLib from "ulid";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class ChatbotGateway {
|
||||||
|
private io: Server;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
private readonly CHATBOT_ULID_COOKIE_NAME = "chatbot_session_id";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService,
|
||||||
|
@inject(IOCTYPES.ChatbotWebSocketAuthService) private wsAuthService: WebSocketAuthService,
|
||||||
|
) {
|
||||||
|
this.logger = new Logger("ChatbotGateway");
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialize(server: HttpServer): Server {
|
||||||
|
this.io = new Server(server, {
|
||||||
|
path: "/ws-chatbot",
|
||||||
|
cors: {
|
||||||
|
origin: true,
|
||||||
|
allowedHeaders: ["Authorization"],
|
||||||
|
credentials: true,
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.io.on("connection", (socket: Socket) => this.handleConnection(socket));
|
||||||
|
this.logger.info("ChatbotGateway initialized on /ws-chatbot");
|
||||||
|
return this.io;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleConnection(socket: Socket): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Try to authenticate if token is provided
|
||||||
|
const token = socket.handshake.query.token as string;
|
||||||
|
let ulid: string | undefined;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// Authenticated connection
|
||||||
|
const authResult = await this.wsAuthService.authenticateClient(socket as AuthenticatedSocket);
|
||||||
|
if (!authResult.success) {
|
||||||
|
this.wsAuthService.handleAuthenticationFailure(socket, authResult.error || "Authentication failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.wsAuthService.emitAuthenticationSuccess(socket, authResult.user!);
|
||||||
|
ulid = authResult.user!.id;
|
||||||
|
} else {
|
||||||
|
// Anonymous connection - generate or get ULID from cookie
|
||||||
|
const cookieHeader = socket.handshake.headers.cookie;
|
||||||
|
ulid = this.extractUlidFromCookie(cookieHeader);
|
||||||
|
if (!ulid) {
|
||||||
|
ulid = ulidLib.ulid();
|
||||||
|
}
|
||||||
|
socket.data = { user: { id: ulid, sub: ulid } };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Client connected: ${socket.id}, ULID: ${ulid}`);
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
this.registerEventHandlers(socket, ulid);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Connection error", error);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.ERROR, {
|
||||||
|
status: "error",
|
||||||
|
message: "Connection failed",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractUlidFromCookie(cookieHeader?: string | string[]): string | undefined {
|
||||||
|
if (!cookieHeader || typeof cookieHeader !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = cookieHeader.split(";").reduce(
|
||||||
|
(acc, cookie) => {
|
||||||
|
const [key, value] = cookie.trim().split("=");
|
||||||
|
if (key && value) {
|
||||||
|
acc[key] = decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return cookies[this.CHATBOT_ULID_COOKIE_NAME];
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerEventHandlers(socket: Socket, ulid: string): void {
|
||||||
|
// Session management
|
||||||
|
socket.on(WEBSOCKET_EVENTS.CREATE_SESSION, () => this.handleCreateSession(socket, ulid));
|
||||||
|
socket.on(WEBSOCKET_EVENTS.JOIN_CHAT, (sessionId: string) => this.handleJoinChat(socket, ulid, sessionId));
|
||||||
|
socket.on(WEBSOCKET_EVENTS.LEAVE_CHAT, (sessionId: string) => this.handleLeaveChat(socket, sessionId));
|
||||||
|
|
||||||
|
// Message handling
|
||||||
|
socket.on(WEBSOCKET_EVENTS.SEND_MESSAGE, (data: SendMessageDto) => this.handleSendMessage(socket, ulid, data));
|
||||||
|
socket.on(WEBSOCKET_EVENTS.SEND_MESSAGE + "_stream", (data: SendMessageDto) => this.handleSendMessageStream(socket, ulid, data));
|
||||||
|
|
||||||
|
// Typing indicators
|
||||||
|
socket.on(WEBSOCKET_EVENTS.TYPING_START, (sessionId: string) => this.handleTypingStart(socket, sessionId));
|
||||||
|
socket.on(WEBSOCKET_EVENTS.TYPING_STOP, (sessionId: string) => this.handleTypingStop(socket, sessionId));
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
socket.on(WEBSOCKET_EVENTS.DISCONNECT, () => this.handleDisconnect(socket));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCreateSession(socket: Socket, ulid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const session = await this.chatbotService.createChatSession(ulid);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.SESSION_CREATED, {
|
||||||
|
status: "success",
|
||||||
|
data: session,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Error creating session", error);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.SESSION_ERROR, {
|
||||||
|
status: "error",
|
||||||
|
message: "Failed to create session",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleJoinChat(socket: Socket, ulid: string, sessionId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const session = await this.chatbotService.getChatSession(sessionId, ulid);
|
||||||
|
socket.join(`session_${sessionId}`);
|
||||||
|
(socket as AuthenticatedSocket).data.sessionId = sessionId;
|
||||||
|
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.CHAT_JOINED, {
|
||||||
|
status: "success",
|
||||||
|
data: session,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Notify others in the session (if any)
|
||||||
|
socket.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.USER_JOINED, {
|
||||||
|
status: "success",
|
||||||
|
data: { userId: ulid, sessionId },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Error joining chat", error);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.SESSION_ERROR, {
|
||||||
|
status: "error",
|
||||||
|
message: "Failed to join chat session",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLeaveChat(socket: Socket, sessionId: string): void {
|
||||||
|
socket.leave(`session_${sessionId}`);
|
||||||
|
delete (socket as AuthenticatedSocket).data.sessionId;
|
||||||
|
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.CHAT_LEFT, {
|
||||||
|
status: "success",
|
||||||
|
message: "Left chat session",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
socket.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.USER_LEFT, {
|
||||||
|
status: "success",
|
||||||
|
data: { sessionId },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSendMessage(socket: Socket, ulid: string, data: SendMessageDto): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Save user message
|
||||||
|
const userMessage = await this.chatbotService.sendMessage(ulid, data);
|
||||||
|
|
||||||
|
// Emit user message to client
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.MESSAGE_RECEIVED, {
|
||||||
|
status: "success",
|
||||||
|
data: userMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Emit to others in the session (if any)
|
||||||
|
socket.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.MESSAGE_RECEIVED, {
|
||||||
|
status: "success",
|
||||||
|
data: userMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Generate bot response asynchronously
|
||||||
|
this.generateBotResponseAsync(socket, data.sessionId, userMessage.id, ulid);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Error sending message", error);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.MESSAGE_ERROR, {
|
||||||
|
status: "error",
|
||||||
|
message: "Failed to send message",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSendMessageStream(socket: Socket, ulid: string, data: SendMessageDto): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStream(ulid, data);
|
||||||
|
|
||||||
|
// Emit user message
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.MESSAGE_RECEIVED, {
|
||||||
|
status: "success",
|
||||||
|
data: userMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Emit to others in the session (if any)
|
||||||
|
socket.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.MESSAGE_RECEIVED, {
|
||||||
|
status: "success",
|
||||||
|
data: userMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Start streaming bot response
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.BOT_RESPONSE_START, {
|
||||||
|
status: "success",
|
||||||
|
data: { userMessageId: userMessage.id },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
let fullResponse = "";
|
||||||
|
const stream = await streamGenerator();
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk) {
|
||||||
|
fullResponse += chunk;
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.BOT_RESPONSE_CHUNK, {
|
||||||
|
status: "success",
|
||||||
|
data: { chunk, userMessageId: userMessage.id },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save bot response after streaming completes
|
||||||
|
if (fullResponse) {
|
||||||
|
await this.chatbotService.saveStreamedBotResponse(data.sessionId, userMessage.id, fullResponse, ulid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit final bot response
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.BOT_RESPONSE_END, {
|
||||||
|
status: "success",
|
||||||
|
data: { userMessageId: userMessage.id, fullResponse },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Error sending stream message", error);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.MESSAGE_ERROR, {
|
||||||
|
status: "error",
|
||||||
|
message: "Failed to send message",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateBotResponseAsync(socket: Socket, sessionId: string, userMessageId: string, ulid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get the session to check if it exists
|
||||||
|
const session = await this.chatbotService.getChatSession(sessionId, ulid);
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit that bot is typing
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.BOT_RESPONSE_START, {
|
||||||
|
status: "success",
|
||||||
|
data: { userMessageId },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Generate bot response and wait for it
|
||||||
|
const botMessage = await this.chatbotService.generateBotResponse(sessionId, userMessageId, ulid);
|
||||||
|
|
||||||
|
if (botMessage) {
|
||||||
|
// Emit bot response to the client
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.BOT_RESPONSE, {
|
||||||
|
status: "success",
|
||||||
|
data: botMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
|
||||||
|
// Also emit to others in the session (if any)
|
||||||
|
socket.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE, {
|
||||||
|
status: "success",
|
||||||
|
data: botMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Error generating bot response", error);
|
||||||
|
socket.emit(WEBSOCKET_EVENTS.BOT_RESPONSE, {
|
||||||
|
status: "error",
|
||||||
|
message: "Failed to generate bot response",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTypingStart(socket: Socket, sessionId: string): void {
|
||||||
|
socket.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.TYPING_START, {
|
||||||
|
status: "success",
|
||||||
|
data: { sessionId, userId: socket.data?.user?.id },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTypingStop(socket: Socket, sessionId: string): void {
|
||||||
|
socket.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.TYPING_STOP, {
|
||||||
|
status: "success",
|
||||||
|
data: { sessionId, userId: socket.data?.user?.id },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} as WebSocketResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDisconnect(socket: Socket): void {
|
||||||
|
const user = socket.data?.user;
|
||||||
|
if (user) {
|
||||||
|
this.logger.info(`Client disconnected: ${socket.id}, ULID: ${user.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
export const CHATBOT_CONSTANTS = {
|
||||||
|
DEFAULT_MODEL: "gpt-4o",
|
||||||
|
DEFAULT_TEMPERATURE: 0.7,
|
||||||
|
DEFAULT_MAX_TOKENS: 2000,
|
||||||
|
MAX_CONVERSATION_HISTORY: 20,
|
||||||
|
|
||||||
|
// General system prompt focused on company guidance
|
||||||
|
SYSTEM_PROMPT: `شما یک دستیار هوشمند AI برای پلتفرم DZone هستید که در راهنمایی کاربران درباره شرکتها و صنایع تخصص دارید. شما باید تمامی پاسخها را به زبان فارسی ارائه دهید.
|
||||||
|
|
||||||
|
## وظایف اصلی شما:
|
||||||
|
🏢 **راهنمایی شرکتها**: کمک به کاربران برای یافتن شرکتهای مناسب و اطلاعات تماس آنها
|
||||||
|
🏭 **معرفی صنایع**: ارائه اطلاعات کامل درباره صنایع مختلف و شرکتهای فعال در هر حوزه
|
||||||
|
💼 **محصولات و خدمات**: راهنمایی درباره محصولات و خدمات ارائه شده توسط شرکتها
|
||||||
|
🔍 **جستجو و مقایسه**: کمک به انتخاب بهترین شرکت برای نیازهای خاص کاربران
|
||||||
|
📞 **اطلاعات تماس**: ارائه اطلاعات کامل تماس شامل تلفن، ایمیل و آدرس شرکتها
|
||||||
|
|
||||||
|
## اصول پاسخگویی:
|
||||||
|
✅ همیشه به زبان فارسی پاسخ دهید
|
||||||
|
✅ از اطلاعات پایگاه داده شرکتها و صنایع برای پاسخهای دقیق استفاده کنید
|
||||||
|
✅ در صورت سوال درباره شرکت خاص، اطلاعات کامل آن را ارائه دهید
|
||||||
|
✅ همیشه اطلاعات تماس (تلفن، ایمیل، آدرس) شرکتها را اضافه کنید
|
||||||
|
✅ اگر چیزی نمیدانید، صادقانه اعتراف کنید و راه حل جایگزین پیشنهاد دهید
|
||||||
|
✅ از ایموجیها برای بهتر کردن تجربه کاربر استفاده کنید
|
||||||
|
✅ پاسخهای خود را ساختاربندی و خوانا ارائه دهید
|
||||||
|
|
||||||
|
## قالب پاسخ برای شرکتها:
|
||||||
|
📍 **نام شرکت**: [نام کامل]
|
||||||
|
📞 **تلفن**: [شماره تماس]
|
||||||
|
📧 **ایمیل**: [آدرس ایمیل]
|
||||||
|
🏠 **آدرس**: [آدرس کامل]
|
||||||
|
🏭 **صنعت**: [نوع صنعت]
|
||||||
|
💼 **محصولات/خدمات**: [فهرست محصولات و خدمات]
|
||||||
|
|
||||||
|
اگر اطلاعات شرکتها یا صنایع مربوطه از پایگاه داده در دسترس است، حتماً از آن استفاده کرده و منبع آن را مشخص کنید.`,
|
||||||
|
|
||||||
|
// New company guidance system prompt
|
||||||
|
COMPANY_GUIDANCE_SYSTEM_PROMPT: `شما یک دستیار هوشمند برای راهنمایی کاربران درباره شرکتها و صنایع موجود در پلتفرم DZone هستید.
|
||||||
|
|
||||||
|
## ماموریت شما:
|
||||||
|
- راهنمایی کاربران برای یافتن شرکتهای مناسب
|
||||||
|
- ارائه اطلاعات کامل درباره شرکتها، محصولات و خدمات
|
||||||
|
- معرفی صنایع مختلف و شرکتهای فعال در هر حوزه
|
||||||
|
- کمک به انتخاب بهترین شرکت برای نیازهای کاربران
|
||||||
|
|
||||||
|
## اطلاعات شرکتها و صنایع:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
## تاریخچه مکالمه:
|
||||||
|
{conversation_history}
|
||||||
|
|
||||||
|
## سوال کاربر:
|
||||||
|
{question}
|
||||||
|
|
||||||
|
## دستورالعمل پاسخ:
|
||||||
|
بر اساس اطلاعات شرکتها و صنایع موجود، پاسخی جامع و کاربردی به زبان فارسی ارائه دهید:
|
||||||
|
|
||||||
|
**نکات مهم:**
|
||||||
|
- اگر کاربر درباره شرکت خاصی سوال میپرسد، اطلاعات کامل آن شرکت (نام، آدرس، تلفن، محصولات، خدمات) را ارائه دهید
|
||||||
|
- اگر سوال درباره صنعت خاصی است، شرکتهای فعال در آن صنعت را معرفی کنید
|
||||||
|
- در صورت جستجوی محصول یا خدمت، شرکتهای مرتبط را پیشنهاد دهید
|
||||||
|
- همیشه اطلاعات تماس (تلفن، ایمیل، آدرس) شرکتها را ارائه دهید
|
||||||
|
- از ایموجیهای مناسب برای بهتر شدن خوانایی استفاده کنید
|
||||||
|
- پاسخ خود را ساختاربندی و منظم ارائه دهید
|
||||||
|
|
||||||
|
پاسخ شما:`,
|
||||||
|
|
||||||
|
// Company data templates
|
||||||
|
COMPANY_DATA_TEMPLATES: {
|
||||||
|
COMPANY_INFO: `نام شرکت: {name}
|
||||||
|
مدیر عامل: {ceo}
|
||||||
|
ایمیل: {email}
|
||||||
|
تلفن: {phone}
|
||||||
|
شماره ثبت: {registrationNumber}
|
||||||
|
تاریخ تاسیس: {establishmentDate}
|
||||||
|
آدرس: {address}
|
||||||
|
وبسایت: {website}
|
||||||
|
توضیحات: {description}
|
||||||
|
صنعت فعالیت: {industry}
|
||||||
|
وضعیت: {status}
|
||||||
|
کسب و کار والد: {business}
|
||||||
|
|
||||||
|
این شرکت در پلتفرم DZone ثبت شده و آماده ارائه خدمات و محصولات خود به مشتریان میباشد.`,
|
||||||
|
|
||||||
|
COMPANY_PRODUCT: `محصول شرکت {companyName}:
|
||||||
|
نام محصول: {productTitle}
|
||||||
|
شرکت تولیدکننده: {companyName}
|
||||||
|
صنعت: {industry}
|
||||||
|
توضیحات شرکت: {companyDescription}
|
||||||
|
آدرس شرکت: {companyAddress}
|
||||||
|
تلفن تماس: {companyPhone}
|
||||||
|
ایمیل: {companyEmail}
|
||||||
|
|
||||||
|
این محصول توسط شرکت {companyName} در صنعت {industry} تولید و ارائه میشود.`,
|
||||||
|
|
||||||
|
COMPANY_SERVICE: `خدمات شرکت {companyName}:
|
||||||
|
نام خدمت: {serviceTitle}
|
||||||
|
شرکت ارائهدهنده: {companyName}
|
||||||
|
صنعت: {industry}
|
||||||
|
توضیحات شرکت: {companyDescription}
|
||||||
|
آدرس شرکت: {companyAddress}
|
||||||
|
تلفن تماس: {companyPhone}
|
||||||
|
ایمیل: {companyEmail}
|
||||||
|
|
||||||
|
این خدمت توسط شرکت {companyName} در صنعت {industry} ارائه میشود.`,
|
||||||
|
|
||||||
|
INDUSTRY_INFO: `صنعت: {title}
|
||||||
|
وضعیت: {status}
|
||||||
|
تعداد شرکتهای فعال: {companiesCount}
|
||||||
|
کسب و کار والد: {business}
|
||||||
|
|
||||||
|
شرکتهای فعال در این صنعت:
|
||||||
|
{companiesList}
|
||||||
|
|
||||||
|
این صنعت شامل شرکتهایی است که در حوزه {title} فعالیت میکنند و خدمات و محصولات مختلفی ارائه میدهند.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Company guidance documents
|
||||||
|
COMPANY_GUIDANCE_DOCUMENTS: {
|
||||||
|
COMPANY_GUIDE: `راهنمای شرکتها در پلتفرم DZone:
|
||||||
|
|
||||||
|
🏢 **اطلاعات شرکتها**:
|
||||||
|
در پلتفرم DZone شرکتهای مختلفی در صنایع متنوع فعالیت میکنند. هر شرکت دارای اطلاعات کاملی شامل:
|
||||||
|
- نام و مشخصات کامل شرکت
|
||||||
|
- اطلاعات تماس (تلفن، ایمیل، آدرس)
|
||||||
|
- صنعت فعالیت و حوزه کاری
|
||||||
|
- محصولات و خدمات ارائه شده
|
||||||
|
- وضعیت فعالیت و مجوزهای لازم
|
||||||
|
|
||||||
|
💼 **محصولات و خدمات**:
|
||||||
|
هر شرکت میتواند محصولات و خدمات متنوعی ارائه دهد:
|
||||||
|
- محصولات: کالاها و تولیدات شرکت
|
||||||
|
- خدمات: سرویسها و راهکارهای ارائه شده
|
||||||
|
- اطلاعات تماس برای استعلام و سفارش
|
||||||
|
- جزئیات کامل درباره هر محصول و خدمت
|
||||||
|
|
||||||
|
🏭 **دستهبندی صنایع**:
|
||||||
|
شرکتها بر اساس صنعت فعالیت دستهبندی شدهاند:
|
||||||
|
- هر صنعت شامل شرکتهای مرتبط
|
||||||
|
- امکان جستجو بر اساس نوع صنعت
|
||||||
|
- مشاهده تمام شرکتهای یک صنعت خاص
|
||||||
|
- اطلاعات کامل درباره حوزه فعالیت`,
|
||||||
|
|
||||||
|
COMPANY_FAQ: `سوالات متداول درباره شرکتها:
|
||||||
|
|
||||||
|
❓ **چگونه اطلاعات یک شرکت را پیدا کنم؟**
|
||||||
|
میتوانید با ذکر نام شرکت یا صنعت مورد نظر، اطلاعات کامل شامل آدرس، تلفن و محصولات آن را دریافت کنید.
|
||||||
|
|
||||||
|
❓ **چه شرکتهایی در یک صنعت خاص فعالیت میکنند؟**
|
||||||
|
با ذکر نام صنعت، لیست کامل شرکتهای فعال در آن حوزه را مشاهده خواهید کرد.
|
||||||
|
|
||||||
|
❓ **چگونه با یک شرکت تماس بگیرم؟**
|
||||||
|
اطلاعات تماس شامل تلفن، ایمیل و آدرس تمام شرکتها در پلتفرم موجود است.
|
||||||
|
|
||||||
|
❓ **چه محصولات و خدماتی ارائه میشود؟**
|
||||||
|
هر شرکت لیست کاملی از محصولات و خدمات خود ارائه داده که قابل مشاهده است.
|
||||||
|
|
||||||
|
❓ **چگونه شرکت مناسب برای نیاز خودم پیدا کنم؟**
|
||||||
|
با توصیف نیاز خود یا نوع محصول/خدمت مورد نظر، شرکتهای مرتبط را معرفی خواهم کرد.`,
|
||||||
|
|
||||||
|
SEARCH_GUIDE: `راهنمای جستجو و یافتن شرکتها:
|
||||||
|
|
||||||
|
🔍 **نحوه جستجو**:
|
||||||
|
- جستجو بر اساس نام شرکت
|
||||||
|
- جستجو بر اساس نوع صنعت
|
||||||
|
- جستجو بر اساس محصول یا خدمت
|
||||||
|
- جستجو بر اساس منطقه جغرافیایی
|
||||||
|
|
||||||
|
📊 **اطلاعات قابل دریافت**:
|
||||||
|
- اطلاعات کامل شرکت (نام، آدرس، تلفن)
|
||||||
|
- لیست محصولات و توضیحات
|
||||||
|
- لیست خدمات ارائه شده
|
||||||
|
- صنعت و حوزه فعالیت
|
||||||
|
- وضعیت فعالیت شرکت
|
||||||
|
|
||||||
|
🎯 **توصیههای مفید**:
|
||||||
|
- برای یافتن شرکت مناسب، نیاز خود را واضح بیان کنید
|
||||||
|
- از کلمات کلیدی مرتبط با صنعت استفاده کنید
|
||||||
|
- در صورت نیاز، اطلاعات تماس برای پیگیری بیشتر درخواست کنید
|
||||||
|
- برای مقایسه، چندین شرکت در یک صنعت را بررسی کنید`,
|
||||||
|
|
||||||
|
INDUSTRY_GUIDE: `راهنمای صنایع و حوزههای فعالیت:
|
||||||
|
|
||||||
|
🏭 **انواع صنایع موجود**:
|
||||||
|
در پلتفرم DZone شرکتهای مختلفی در صنایع متنوع حضور دارند:
|
||||||
|
- صنایع تولیدی و ساختمانی
|
||||||
|
- خدمات فناوری اطلاعات
|
||||||
|
- صنایع غذایی و کشاورزی
|
||||||
|
- خدمات مالی و بیمه
|
||||||
|
- حمل و نقل و لجستیک
|
||||||
|
- آموزش و تربیت
|
||||||
|
- سلامت و درمان
|
||||||
|
- و سایر صنایع
|
||||||
|
|
||||||
|
💡 **مزایای دستهبندی صنعتی**:
|
||||||
|
- یافتن آسان شرکتهای مرتبط
|
||||||
|
- مقایسه خدمات در یک صنعت
|
||||||
|
- انتخاب بهترین گزینه برای نیاز شما
|
||||||
|
- دسترسی به اطلاعات تخصصی هر حوزه
|
||||||
|
|
||||||
|
📈 **نحوه استفاده**:
|
||||||
|
- ابتدا صنعت مورد نظر خود را مشخص کنید
|
||||||
|
- سپس شرکتهای فعال در آن صنعت را بررسی کنید
|
||||||
|
- محصولات و خدمات مناسب را انتخاب کنید
|
||||||
|
- برای اطلاعات بیشتر با شرکت تماس بگیرید`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Logging messages for LangChain service
|
||||||
|
LANGCHAIN_MESSAGES: {
|
||||||
|
LOADING_DATA: "Loading company and industry data for model training...",
|
||||||
|
NO_DATA_FOUND: "No data found for model training",
|
||||||
|
DOCUMENTS_SPLIT: "{totalDocs} documents split into {chunks} chunks",
|
||||||
|
VECTOR_STORE_READY: "Vector store successfully initialized - ready to guide users about companies and industries",
|
||||||
|
VECTOR_STORE_ERROR: "Error initializing vector store",
|
||||||
|
DOCUMENTS_LOADED: "{count} documents loaded from company and industry database",
|
||||||
|
LOADING_ERROR: "Error loading company and industry documents",
|
||||||
|
SERVICE_NOT_INITIALIZED: "LangChain service is not initialized",
|
||||||
|
RESPONSE_ERROR: "Error generating LangChain response",
|
||||||
|
STREAM_RESPONSE_ERROR: "Error generating LangChain stream response",
|
||||||
|
REFRESHING_VECTOR_STORE: "Refreshing vector store with new company and industry data...",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Source type labels in Farsi
|
||||||
|
SOURCE_TYPE_LABELS: {
|
||||||
|
company: "شرکت",
|
||||||
|
product: "محصول",
|
||||||
|
service: "خدمت",
|
||||||
|
industry: "صنعت",
|
||||||
|
company_guide: "راهنمای شرکتها",
|
||||||
|
company_faq: "سوالات متداول",
|
||||||
|
search_guide: "راهنمای جستجو",
|
||||||
|
industry_guide: "راهنمای صنایع",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Default values for missing data
|
||||||
|
DEFAULT_VALUES: {
|
||||||
|
NO_WEBSITE: "ندارد",
|
||||||
|
UNSPECIFIED: "مشخص نشده",
|
||||||
|
ACTIVE: "فعال",
|
||||||
|
INACTIVE: "غیرفعال",
|
||||||
|
},
|
||||||
|
|
||||||
|
// WEBSOCKET_EVENTS: {
|
||||||
|
// // Connection events
|
||||||
|
// CONNECT: "connect",
|
||||||
|
// DISCONNECT: "disconnect",
|
||||||
|
// CONNECTION_ERROR: "connection_failed",
|
||||||
|
|
||||||
|
// // Authentication events
|
||||||
|
// AUTHENTICATE: "authenticate",
|
||||||
|
// AUTHENTICATED: "authenticated",
|
||||||
|
// UNAUTHORIZED: "unauthorized",
|
||||||
|
|
||||||
|
// // Session events
|
||||||
|
// CREATE_SESSION: "create_session",
|
||||||
|
// SESSION_CREATED: "session_created",
|
||||||
|
// JOIN_CHAT: "join_chat",
|
||||||
|
// CHAT_JOINED: "chat_joined",
|
||||||
|
// LEAVE_CHAT: "leave_chat",
|
||||||
|
// CHAT_LEFT: "chat_left",
|
||||||
|
|
||||||
|
// // Message events
|
||||||
|
// SEND_MESSAGE: "send_message",
|
||||||
|
// MESSAGE_RECEIVED: "message_received",
|
||||||
|
// BOT_RESPONSE_START: "bot_response_start",
|
||||||
|
// BOT_RESPONSE_CHUNK: "bot_response_chunk",
|
||||||
|
// BOT_RESPONSE_END: "bot_response_end",
|
||||||
|
// BOT_RESPONSE: "bot_response",
|
||||||
|
|
||||||
|
// // Status events
|
||||||
|
// TYPING_START: "typing_start",
|
||||||
|
// TYPING_STOP: "typing_stop",
|
||||||
|
// USER_JOINED: "user_joined",
|
||||||
|
// USER_LEFT: "user_left",
|
||||||
|
|
||||||
|
// // Error events
|
||||||
|
// ERROR: "error",
|
||||||
|
// WEBSOCKET_ERROR: "websocket_error",
|
||||||
|
// SESSION_ERROR: "session_error",
|
||||||
|
// MESSAGE_ERROR: "message_error",
|
||||||
|
// },
|
||||||
|
|
||||||
|
ERROR_MESSAGES: {
|
||||||
|
SESSION_NOT_FOUND: "جلسه چت یافت نشد",
|
||||||
|
UNAUTHORIZED_ACCESS: "دسترسی غیرمجاز به جلسه چت",
|
||||||
|
MESSAGE_TOO_LONG: "پیام از حداکثر طول مجاز تجاوز کرده است",
|
||||||
|
RATE_LIMIT_EXCEEDED: "تعداد پیامهای ارسالی بیش از حد مجاز است",
|
||||||
|
LLM_SERVICE_ERROR: "سرویس هوش مصنوعی موقتاً در دسترس نیست",
|
||||||
|
INVALID_TOKEN: "توکن احراز هویت نامعتبر است",
|
||||||
|
AUTHENTICATION_REQUIRED: "احراز هویت ضروری است",
|
||||||
|
SESSION_CREATION_FAILED: "ایجاد جلسه چت با شکست مواجه شد",
|
||||||
|
CONNECTION_FAILED: "اتصال برقرار نشد",
|
||||||
|
WEBSOCKET_ERROR: "خطا در ارتباط WebSocket",
|
||||||
|
},
|
||||||
|
|
||||||
|
SUCCESS_MESSAGES: {
|
||||||
|
SESSION_CREATED: "جلسه چت با موفقیت ایجاد شد",
|
||||||
|
CHAT_JOINED: "با موفقیت به چت متصل شدید",
|
||||||
|
MESSAGE_SENT: "پیام ارسال شد",
|
||||||
|
AUTHENTICATED: "احراز هویت موفقیتآمیز",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const enum WEBSOCKET_EVENTS {
|
||||||
|
CONNECT = "connect",
|
||||||
|
DISCONNECT = "disconnect",
|
||||||
|
CONNECTION_ERROR = "connection_failed",
|
||||||
|
|
||||||
|
// Authentication events
|
||||||
|
AUTHENTICATE = "authenticate",
|
||||||
|
AUTHENTICATED = "authenticated",
|
||||||
|
UNAUTHORIZED = "unauthorized",
|
||||||
|
|
||||||
|
// Session events
|
||||||
|
CREATE_SESSION = "create_session",
|
||||||
|
SESSION_CREATED = "session_created",
|
||||||
|
JOIN_CHAT = "join_chat",
|
||||||
|
CHAT_JOINED = "chat_joined",
|
||||||
|
LEAVE_CHAT = "leave_chat",
|
||||||
|
CHAT_LEFT = "chat_left",
|
||||||
|
|
||||||
|
// Message events
|
||||||
|
SEND_MESSAGE = "send_message",
|
||||||
|
MESSAGE_RECEIVED = "message_received",
|
||||||
|
BOT_RESPONSE_START = "bot_response_start",
|
||||||
|
BOT_RESPONSE_CHUNK = "bot_response_chunk",
|
||||||
|
BOT_RESPONSE_END = "bot_response_end",
|
||||||
|
BOT_RESPONSE = "bot_response",
|
||||||
|
|
||||||
|
// Status events
|
||||||
|
TYPING_START = "typing_start",
|
||||||
|
TYPING_STOP = "typing_stop",
|
||||||
|
USER_JOINED = "user_joined",
|
||||||
|
USER_LEFT = "user_left",
|
||||||
|
|
||||||
|
// Error events
|
||||||
|
ERROR = "error",
|
||||||
|
WEBSOCKET_ERROR = "websocket_error",
|
||||||
|
SESSION_ERROR = "session_error",
|
||||||
|
MESSAGE_ERROR = "message_error",
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export interface IChatbotResponse {
|
||||||
|
message: string;
|
||||||
|
confidence: number;
|
||||||
|
sources?: string[];
|
||||||
|
tokensUsed: number;
|
||||||
|
context?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatContext {
|
||||||
|
userId: string;
|
||||||
|
sessionId: string;
|
||||||
|
conversationHistory: IChatMessage[];
|
||||||
|
userPreferences?: Record<string, any>;
|
||||||
|
businessContext?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatMessage {
|
||||||
|
content: string;
|
||||||
|
type: "user" | "bot" | "system";
|
||||||
|
timestamp: Date;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILLMConfig {
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
maxTokens: number;
|
||||||
|
topP: number;
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataSource {
|
||||||
|
query: (question: string, context?: Record<string, any>) => Promise<string[]>;
|
||||||
|
type: "database" | "vector" | "api";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Socket } from "socket.io";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended Socket interface with authenticated user data
|
||||||
|
*/
|
||||||
|
export interface AuthenticatedSocket extends Socket {
|
||||||
|
data: {
|
||||||
|
user: { id: string; sub: string };
|
||||||
|
sessionId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User connection information stored in gateway
|
||||||
|
*/
|
||||||
|
export interface ConnectedUserInfo {
|
||||||
|
userId: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket error context for logging and debugging
|
||||||
|
*/
|
||||||
|
export interface WebSocketErrorContext {
|
||||||
|
clientId: string;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
event?: string;
|
||||||
|
data?: any;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket event response structure
|
||||||
|
*/
|
||||||
|
export interface WebSocketResponse<T = any> {
|
||||||
|
status: "success" | "error";
|
||||||
|
message?: string;
|
||||||
|
data?: T;
|
||||||
|
timestamp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication result for WebSocket connections
|
||||||
|
*/
|
||||||
|
export interface AuthenticationResult {
|
||||||
|
success: boolean;
|
||||||
|
user?: { id: string; sub: string };
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
export enum MessageType {
|
||||||
|
USER = "user",
|
||||||
|
BOT = "bot",
|
||||||
|
SYSTEM = "system",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageStatus {
|
||||||
|
SENT = "sent",
|
||||||
|
DELIVERED = "delivered",
|
||||||
|
READ = "read",
|
||||||
|
FAILED = "failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatMessage {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
content: string;
|
||||||
|
type: MessageType;
|
||||||
|
status: MessageStatus;
|
||||||
|
session: Types.ObjectId;
|
||||||
|
sender?: Types.ObjectId;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
responseToId?: string;
|
||||||
|
tokensUsed: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
export enum ChatSessionStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
CLOSED = "closed",
|
||||||
|
ARCHIVED = "archived",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatSession {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
title: string;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
status: ChatSessionStatus;
|
||||||
|
context?: Record<string, any>;
|
||||||
|
lastMessageAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
import { inject, injectable } from "inversify";
|
||||||
|
import * as ulidLib from "ulid";
|
||||||
|
|
||||||
|
import { LLMService } from "./llm.service";
|
||||||
|
import { BadRequestError, NotFoundError } from "../../../core/app/app.errors";
|
||||||
|
import { Logger } from "../../../core/logging/logger";
|
||||||
|
import { IOCTYPES } from "../../../IOC/ioc.types";
|
||||||
|
import { CacheService } from "../../../utils/cache.service";
|
||||||
|
import { SendMessageDto } from "../DTO/send-message.dto";
|
||||||
|
import { IChatContext } from "../interfaces/chatbot.interface";
|
||||||
|
import { MessageStatus, MessageType } from "../models/Abstraction/IChatMessage";
|
||||||
|
import { ChatSessionStatus } from "../models/Abstraction/IChatSession";
|
||||||
|
|
||||||
|
interface CachedSession {
|
||||||
|
id: string;
|
||||||
|
ulid: string;
|
||||||
|
status: ChatSessionStatus;
|
||||||
|
lastMessageAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedMessage {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
type: MessageType;
|
||||||
|
status: MessageStatus;
|
||||||
|
sessionId: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
responseToId?: string;
|
||||||
|
tokensUsed: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class ChatbotService {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
private readonly CACHE_TTL_SECONDS = 15 * 60; // 15 minutes
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@inject(IOCTYPES.ChatbotLLMService) private llmService: LLMService,
|
||||||
|
@inject(IOCTYPES.CacheService) private cacheService: CacheService,
|
||||||
|
) {
|
||||||
|
this.logger = new Logger("ChatbotService");
|
||||||
|
this.logger.info("Using OpenAI as LLM provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSessionCacheKey(ulid: string, sessionId: string): string {
|
||||||
|
return `chatbot:session:${ulid}:${sessionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSessionsListCacheKey(ulid: string): string {
|
||||||
|
return `chatbot:sessions:${ulid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMessagesCacheKey(sessionId: string): string {
|
||||||
|
return `chatbot:messages:${sessionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createChatSession(ulid: string) {
|
||||||
|
const sessionId = ulidLib.ulid();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const session: CachedSession = {
|
||||||
|
id: sessionId,
|
||||||
|
ulid,
|
||||||
|
status: ChatSessionStatus.ACTIVE,
|
||||||
|
lastMessageAt: now,
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save session to cache
|
||||||
|
await this.cacheService.setAsync(this.getSessionCacheKey(ulid, sessionId), JSON.stringify(session), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Add to sessions list
|
||||||
|
const sessionsListKey = this.getSessionsListCacheKey(ulid);
|
||||||
|
const existingSessionsJson = await this.cacheService.getAsync(sessionsListKey);
|
||||||
|
const sessionsList: string[] = existingSessionsJson ? JSON.parse(existingSessionsJson) : [];
|
||||||
|
sessionsList.push(sessionId);
|
||||||
|
await this.cacheService.setAsync(sessionsListKey, JSON.stringify(sessionsList), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Initialize messages array for this session
|
||||||
|
await this.cacheService.setAsync(this.getMessagesCacheKey(sessionId), JSON.stringify([]), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
return this.mapSessionToDto(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserChatSessions(ulid: string, limit = 10) {
|
||||||
|
const sessionsListKey = this.getSessionsListCacheKey(ulid);
|
||||||
|
const sessionsListJson = await this.cacheService.getAsync(sessionsListKey);
|
||||||
|
|
||||||
|
if (!sessionsListJson) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionIds: string[] = JSON.parse(sessionsListJson);
|
||||||
|
const sessions: CachedSession[] = [];
|
||||||
|
|
||||||
|
// Get sessions in reverse order (newest first)
|
||||||
|
for (let i = sessionIds.length - 1; i >= 0 && sessions.length < limit; i--) {
|
||||||
|
const sessionId = sessionIds[i];
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
|
||||||
|
if (sessionJson) {
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
// Get messages for this session
|
||||||
|
const messagesJson = await this.cacheService.getAsync(this.getMessagesCacheKey(sessionId));
|
||||||
|
const messages: CachedMessage[] = messagesJson ? JSON.parse(messagesJson) : [];
|
||||||
|
sessions.push({ ...session, messages } as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions.map((session) => this.mapSessionToDto(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatSession(sessionId: string, ulid: string) {
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
|
||||||
|
if (!sessionJson) {
|
||||||
|
throw new NotFoundError("Chat session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
|
||||||
|
// Get messages for this session
|
||||||
|
const messagesJson = await this.cacheService.getAsync(this.getMessagesCacheKey(sessionId));
|
||||||
|
const messages: CachedMessage[] = messagesJson ? JSON.parse(messagesJson) : [];
|
||||||
|
|
||||||
|
return this.mapSessionToDto({ ...session, messages } as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(ulid: string, sendDto: SendMessageDto) {
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sendDto.sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
|
||||||
|
if (!sessionJson) {
|
||||||
|
throw new NotFoundError("Chat session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
|
||||||
|
if (session.status !== ChatSessionStatus.ACTIVE) {
|
||||||
|
throw new BadRequestError("Cannot send message to inactive session");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user message
|
||||||
|
const messageId = ulidLib.ulid();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const userMessage: CachedMessage = {
|
||||||
|
id: messageId,
|
||||||
|
content: sendDto.content,
|
||||||
|
type: MessageType.USER,
|
||||||
|
status: MessageStatus.SENT,
|
||||||
|
sessionId: sendDto.sessionId,
|
||||||
|
responseToId: sendDto.responseToId,
|
||||||
|
metadata: sendDto.metadata,
|
||||||
|
tokensUsed: 0,
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save message to cache
|
||||||
|
const messagesKey = this.getMessagesCacheKey(sendDto.sessionId);
|
||||||
|
const messagesJson = await this.cacheService.getAsync(messagesKey);
|
||||||
|
const messages: CachedMessage[] = messagesJson ? JSON.parse(messagesJson) : [];
|
||||||
|
messages.push(userMessage);
|
||||||
|
await this.cacheService.setAsync(messagesKey, JSON.stringify(messages), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Update session last message time
|
||||||
|
session.lastMessageAt = now;
|
||||||
|
await this.cacheService.setAsync(sessionKey, JSON.stringify(session), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Note: Bot response will be generated by the gateway after emitting user message
|
||||||
|
|
||||||
|
return this.mapMessageToDto(userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageStream(ulid: string, sendDto: SendMessageDto) {
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sendDto.sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
|
||||||
|
if (!sessionJson) {
|
||||||
|
throw new NotFoundError("Chat session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
|
||||||
|
if (session.status !== ChatSessionStatus.ACTIVE) {
|
||||||
|
throw new BadRequestError("Cannot send message to inactive session");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user message
|
||||||
|
const messageId = ulidLib.ulid();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const userMessage: CachedMessage = {
|
||||||
|
id: messageId,
|
||||||
|
content: sendDto.content,
|
||||||
|
type: MessageType.USER,
|
||||||
|
status: MessageStatus.SENT,
|
||||||
|
sessionId: sendDto.sessionId,
|
||||||
|
responseToId: sendDto.responseToId,
|
||||||
|
metadata: sendDto.metadata,
|
||||||
|
tokensUsed: 0,
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save message to cache
|
||||||
|
const messagesKey = this.getMessagesCacheKey(sendDto.sessionId);
|
||||||
|
const messagesJson = await this.cacheService.getAsync(messagesKey);
|
||||||
|
const messages: CachedMessage[] = messagesJson ? JSON.parse(messagesJson) : [];
|
||||||
|
messages.push(userMessage);
|
||||||
|
await this.cacheService.setAsync(messagesKey, JSON.stringify(messages), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Update session last message time
|
||||||
|
session.lastMessageAt = now;
|
||||||
|
await this.cacheService.setAsync(sessionKey, JSON.stringify(session), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Get conversation history for context
|
||||||
|
const history = messages.slice(-20).map((msg) => ({
|
||||||
|
content: msg.content,
|
||||||
|
type: msg.type as "user" | "bot" | "system",
|
||||||
|
timestamp: new Date(msg.createdAt),
|
||||||
|
metadata: msg.metadata,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Build context for LLM
|
||||||
|
const context: IChatContext = {
|
||||||
|
userId: ulid,
|
||||||
|
sessionId: sendDto.sessionId,
|
||||||
|
conversationHistory: history,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a function that generates the stream using LLM service
|
||||||
|
const streamGenerator = () => this.llmService.generateStreamResponse(sendDto.content, context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userMessage: this.mapMessageToDto(userMessage),
|
||||||
|
streamGenerator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateBotResponse(
|
||||||
|
sessionId: string,
|
||||||
|
userMessageId: string,
|
||||||
|
ulid: string,
|
||||||
|
): Promise<ReturnType<typeof this.mapMessageToDto> | null> {
|
||||||
|
try {
|
||||||
|
// Get messages from cache
|
||||||
|
const messagesKey = this.getMessagesCacheKey(sessionId);
|
||||||
|
const messagesJson = await this.cacheService.getAsync(messagesKey);
|
||||||
|
if (!messagesJson) {
|
||||||
|
throw new NotFoundError("Session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: CachedMessage[] = JSON.parse(messagesJson);
|
||||||
|
const userMessage = messages.find((msg) => msg.id === userMessageId);
|
||||||
|
if (!userMessage) {
|
||||||
|
throw new NotFoundError("User message not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session
|
||||||
|
const sessionsListKey = this.getSessionsListCacheKey(ulid);
|
||||||
|
const sessionsListJson = await this.cacheService.getAsync(sessionsListKey);
|
||||||
|
if (!sessionsListJson) {
|
||||||
|
throw new NotFoundError("Session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
if (!sessionJson) {
|
||||||
|
throw new NotFoundError("Session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
|
||||||
|
// Get conversation history
|
||||||
|
const history = messages.slice(-20).map((msg) => ({
|
||||||
|
content: msg.content,
|
||||||
|
type: msg.type as "user" | "bot" | "system",
|
||||||
|
timestamp: new Date(msg.createdAt),
|
||||||
|
metadata: msg.metadata,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Build context for LLM
|
||||||
|
const context: IChatContext = {
|
||||||
|
userId: ulid,
|
||||||
|
sessionId,
|
||||||
|
conversationHistory: history,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate response using LLM
|
||||||
|
const llmResponse = await this.llmService.generateResponse(userMessage.content, context);
|
||||||
|
|
||||||
|
// Create bot message
|
||||||
|
const botMessageId = ulidLib.ulid();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const botMessage: CachedMessage = {
|
||||||
|
id: botMessageId,
|
||||||
|
content: llmResponse.message,
|
||||||
|
type: MessageType.BOT,
|
||||||
|
status: MessageStatus.SENT,
|
||||||
|
sessionId,
|
||||||
|
responseToId: userMessageId,
|
||||||
|
tokensUsed: llmResponse.tokensUsed,
|
||||||
|
metadata: {
|
||||||
|
confidence: llmResponse.confidence,
|
||||||
|
sources: llmResponse.sources,
|
||||||
|
llmContext: llmResponse.context,
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save bot message to cache
|
||||||
|
messages.push(botMessage);
|
||||||
|
await this.cacheService.setAsync(messagesKey, JSON.stringify(messages), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Update session last message time
|
||||||
|
session.lastMessageAt = now;
|
||||||
|
await this.cacheService.setAsync(sessionKey, JSON.stringify(session), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
this.logger.info(`Generated bot response for session ${sessionId}`);
|
||||||
|
return this.mapMessageToDto(botMessage);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to generate bot response for session ${sessionId}`, error);
|
||||||
|
|
||||||
|
// Create error message
|
||||||
|
const messagesKey = this.getMessagesCacheKey(sessionId);
|
||||||
|
const messagesJson = await this.cacheService.getAsync(messagesKey);
|
||||||
|
const messages: CachedMessage[] = messagesJson ? JSON.parse(messagesJson) : [];
|
||||||
|
|
||||||
|
const errorMessageId = ulidLib.ulid();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const errorMessage: CachedMessage = {
|
||||||
|
id: errorMessageId,
|
||||||
|
content: "متأسفم، در حال حاضر مشکلی در پاسخگویی دارم. لطفاً دوباره تلاش کنید. 🙏",
|
||||||
|
type: MessageType.BOT,
|
||||||
|
status: MessageStatus.FAILED,
|
||||||
|
sessionId,
|
||||||
|
responseToId: userMessageId,
|
||||||
|
tokensUsed: 0,
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.push(errorMessage);
|
||||||
|
await this.cacheService.setAsync(messagesKey, JSON.stringify(messages), this.CACHE_TTL_SECONDS);
|
||||||
|
return this.mapMessageToDto(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeChatSession(sessionId: string, ulid: string) {
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
|
||||||
|
if (!sessionJson) {
|
||||||
|
throw new NotFoundError("Chat session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
session.status = ChatSessionStatus.CLOSED;
|
||||||
|
|
||||||
|
await this.cacheService.setAsync(sessionKey, JSON.stringify(session), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
return { message: "Chat session closed successfully" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async markMessagesAsRead(sessionId: string, ulid: string, messageIds: string[]) {
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
|
||||||
|
if (!sessionJson) {
|
||||||
|
throw new NotFoundError("Chat session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messages and update their status
|
||||||
|
const messagesKey = this.getMessagesCacheKey(sessionId);
|
||||||
|
const messagesJson = await this.cacheService.getAsync(messagesKey);
|
||||||
|
|
||||||
|
if (messagesJson) {
|
||||||
|
const messages: CachedMessage[] = JSON.parse(messagesJson);
|
||||||
|
messages.forEach((msg) => {
|
||||||
|
if (messageIds.includes(msg.id)) {
|
||||||
|
msg.status = MessageStatus.READ;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await this.cacheService.setAsync(messagesKey, JSON.stringify(messages), this.CACHE_TTL_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: "Messages marked as read successfully" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveStreamedBotResponse(sessionId: string, userMessageId: string, botResponse: string, ulid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const messagesKey = this.getMessagesCacheKey(sessionId);
|
||||||
|
const messagesJson = await this.cacheService.getAsync(messagesKey);
|
||||||
|
if (!messagesJson) {
|
||||||
|
this.logger.warn(`Cannot save bot response: session ${sessionId} not found in cache`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: CachedMessage[] = JSON.parse(messagesJson);
|
||||||
|
const botMessageId = ulidLib.ulid();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const botMessage: CachedMessage = {
|
||||||
|
id: botMessageId,
|
||||||
|
content: botResponse,
|
||||||
|
type: MessageType.BOT,
|
||||||
|
status: MessageStatus.SENT,
|
||||||
|
sessionId,
|
||||||
|
responseToId: userMessageId,
|
||||||
|
tokensUsed: 0, // Could estimate tokens if needed
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.push(botMessage);
|
||||||
|
await this.cacheService.setAsync(messagesKey, JSON.stringify(messages), this.CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
|
// Update session last message time
|
||||||
|
const sessionKey = this.getSessionCacheKey(ulid, sessionId);
|
||||||
|
const sessionJson = await this.cacheService.getAsync(sessionKey);
|
||||||
|
if (sessionJson) {
|
||||||
|
const session: CachedSession = JSON.parse(sessionJson);
|
||||||
|
session.lastMessageAt = now;
|
||||||
|
await this.cacheService.setAsync(sessionKey, JSON.stringify(session), this.CACHE_TTL_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Saved streamed bot response for session ${sessionId}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to save streamed bot response for session ${sessionId}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapSessionToDto(session: CachedSession | any) {
|
||||||
|
return {
|
||||||
|
id: session.id,
|
||||||
|
status: session.status,
|
||||||
|
createdAt: session.createdAt,
|
||||||
|
lastMessageAt: session.lastMessageAt,
|
||||||
|
messages: session.messages ? session.messages.map((msg: CachedMessage) => this.mapMessageToDto(msg)) : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapMessageToDto(message: CachedMessage) {
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
content: message.content,
|
||||||
|
type: message.type,
|
||||||
|
status: message.status,
|
||||||
|
createdAt: message.createdAt,
|
||||||
|
responseToId: message.responseToId,
|
||||||
|
metadata: message.metadata,
|
||||||
|
tokensUsed: message.tokensUsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { injectable } from "inversify";
|
||||||
|
|
||||||
|
import { IChatContext } from "../interfaces/chatbot.interface";
|
||||||
|
import { Logger } from "../../../core/logging/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for retrieving relevant context data from the database
|
||||||
|
* This is a simplified version - can be extended to fetch actual data from your database models
|
||||||
|
*/
|
||||||
|
@injectable()
|
||||||
|
export class DataContextService {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger = new Logger("DataContextService");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRelevantContext(_message: string, _context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
|
||||||
|
const relevantData: string[] = [];
|
||||||
|
const sources: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Analyze message to determine what data to fetch
|
||||||
|
// const keywords = this._extractKeywords(_message); // TODO: Use when implementing keyword-based filtering
|
||||||
|
|
||||||
|
// TODO: Implement actual data fetching based on your project's models
|
||||||
|
// For now, this returns empty data - you can extend this to fetch:
|
||||||
|
// - Product data from ProductModel
|
||||||
|
// - Category data from CategoryModel
|
||||||
|
// - Order data from OrderModel
|
||||||
|
// - User data from UserModel
|
||||||
|
// etc.
|
||||||
|
|
||||||
|
// Example: If you want to fetch product data
|
||||||
|
// if (this.hasProductKeywords(keywords)) {
|
||||||
|
// const products = await ProductModel.find({ ... }).limit(5);
|
||||||
|
// products.forEach((product) => {
|
||||||
|
// relevantData.push(`Product: ${product.title_fa} - Price: ${product.price}`);
|
||||||
|
// sources.push("Products Database");
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return { data: relevantData, sources };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Failed to get relevant context", error);
|
||||||
|
return { data: [], sources: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use this method when implementing keyword-based filtering
|
||||||
|
// @ts-expect-error - Method reserved for future use
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
private _extractKeywords(_message: string): string[] {
|
||||||
|
const commonWords = ["the", "is", "at", "which", "on", "a", "an", "and", "or", "but", "in", "with", "to", "for", "of", "as", "by"];
|
||||||
|
|
||||||
|
return _message
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w\s]/g, "")
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((word: string) => word.length > 2 && !commonWords.includes(word));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for keyword detection - can be extended
|
||||||
|
// TODO: Implement when needed
|
||||||
|
// private hasProductKeywords(keywords: string[]): boolean {
|
||||||
|
// const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
|
||||||
|
// return keywords.some(keyword => productTerms.includes(keyword));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private hasOrderKeywords(keywords: string[]): boolean {
|
||||||
|
// const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
|
||||||
|
// return keywords.some(keyword => orderTerms.includes(keyword));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private hasCategoryKeywords(keywords: string[]): boolean {
|
||||||
|
// const categoryTerms = ["category", "type", "kind", "دسته", "دستهبندی"];
|
||||||
|
// return keywords.some(keyword => categoryTerms.includes(keyword));
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { inject, injectable } from "inversify";
|
||||||
|
import OpenAI from "openai";
|
||||||
|
|
||||||
|
import { DataContextService } from "./data-context.service";
|
||||||
|
import { Logger } from "../../../core/logging/logger";
|
||||||
|
import { IOCTYPES } from "../../../IOC/ioc.types";
|
||||||
|
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
|
||||||
|
import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbot.interface";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class LLMService {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
private openai: OpenAI;
|
||||||
|
private config: ILLMConfig;
|
||||||
|
|
||||||
|
constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) {
|
||||||
|
this.logger = new Logger("LLMService");
|
||||||
|
this.config = {
|
||||||
|
model: process.env.OPENAI_MODEL || CHATBOT_CONSTANTS.DEFAULT_MODEL,
|
||||||
|
temperature: Number(process.env.OPENAI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE),
|
||||||
|
maxTokens: Number(process.env.OPENAI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS),
|
||||||
|
topP: Number(process.env.OPENAI_TOP_P || "0.95"),
|
||||||
|
apiKey: process.env.OPENAI_API_KEY || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.config.apiKey) {
|
||||||
|
throw new Error("OPENAI_API_KEY is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openai = new OpenAI({
|
||||||
|
apiKey: this.config.apiKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> {
|
||||||
|
try {
|
||||||
|
// Get relevant data from database
|
||||||
|
const relevantData = await this.dataContextService.getRelevantContext(message, context);
|
||||||
|
|
||||||
|
// Build system instruction with context
|
||||||
|
const systemInstruction = this.buildSystemInstruction(relevantData);
|
||||||
|
|
||||||
|
// Build conversation history
|
||||||
|
const conversationHistory = this.buildConversationHistory(context);
|
||||||
|
|
||||||
|
// Build messages array for OpenAI
|
||||||
|
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: systemInstruction,
|
||||||
|
},
|
||||||
|
...conversationHistory,
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: message,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate response using OpenAI
|
||||||
|
const completion = await this.openai.chat.completions.create({
|
||||||
|
model: this.config.model,
|
||||||
|
messages,
|
||||||
|
temperature: this.config.temperature,
|
||||||
|
max_tokens: this.config.maxTokens,
|
||||||
|
top_p: this.config.topP,
|
||||||
|
});
|
||||||
|
|
||||||
|
const botMessage =
|
||||||
|
completion.choices[0]?.message?.content || "متأسفم، نمیتوانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
|
||||||
|
|
||||||
|
// Calculate token usage from OpenAI response
|
||||||
|
const tokensUsed = (completion.usage?.total_tokens || 0) + this.estimateTokens(systemInstruction);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: botMessage,
|
||||||
|
confidence: this.calculateConfidence(completion),
|
||||||
|
sources: relevantData.sources,
|
||||||
|
tokensUsed,
|
||||||
|
context: {
|
||||||
|
model: this.config.model,
|
||||||
|
relevantDataFound: relevantData.data.length > 0,
|
||||||
|
finishReason: completion.choices[0]?.finish_reason || "unknown",
|
||||||
|
usage: completion.usage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Failed to generate OpenAI response", error);
|
||||||
|
throw new Error("Failed to generate response from AI service");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateStreamResponse(message: string, context: IChatContext): Promise<AsyncIterable<string>> {
|
||||||
|
try {
|
||||||
|
// Get relevant data from database
|
||||||
|
const relevantData = await this.dataContextService.getRelevantContext(message, context);
|
||||||
|
|
||||||
|
// Build system instruction with context
|
||||||
|
const systemInstruction = this.buildSystemInstruction(relevantData);
|
||||||
|
|
||||||
|
// Build conversation history
|
||||||
|
const conversationHistory = this.buildConversationHistory(context);
|
||||||
|
|
||||||
|
// Build messages array for OpenAI
|
||||||
|
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: systemInstruction,
|
||||||
|
},
|
||||||
|
...conversationHistory,
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: message,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate streaming response using OpenAI
|
||||||
|
const stream = await this.openai.chat.completions.create({
|
||||||
|
model: this.config.model,
|
||||||
|
messages,
|
||||||
|
temperature: this.config.temperature,
|
||||||
|
max_tokens: this.config.maxTokens,
|
||||||
|
top_p: this.config.topP,
|
||||||
|
stream: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.createAsyncIterableFromStream(stream);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Failed to generate streaming OpenAI response", error);
|
||||||
|
throw new Error("Failed to generate streaming response from AI service");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSystemInstruction(relevantData: { data: string[]; sources: string[] }): string {
|
||||||
|
let instruction = CHATBOT_CONSTANTS.SYSTEM_PROMPT;
|
||||||
|
|
||||||
|
// Add relevant data context
|
||||||
|
if (relevantData.data.length > 0) {
|
||||||
|
instruction += "\n\nRelevant information from our database:\n";
|
||||||
|
instruction += relevantData.data.map((data, index) => `${index + 1}. ${data}`).join("\n");
|
||||||
|
instruction += "\n\nUse this information to provide accurate and specific answers.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return instruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildConversationHistory(context: IChatContext): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
|
||||||
|
if (!context.conversationHistory || context.conversationHistory.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const history: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [];
|
||||||
|
const recentHistory = context.conversationHistory
|
||||||
|
.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY)
|
||||||
|
.filter((msg) => msg.type !== "system");
|
||||||
|
|
||||||
|
for (const msg of recentHistory) {
|
||||||
|
history.push({
|
||||||
|
role: msg.type === "user" ? "user" : "assistant",
|
||||||
|
content: msg.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async *createAsyncIterableFromStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncIterable<string> {
|
||||||
|
try {
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const content = chunk.choices[0]?.delta?.content;
|
||||||
|
if (content) {
|
||||||
|
yield content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Error in streaming response", error);
|
||||||
|
throw new Error("Failed to process streaming response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateConfidence(completion: OpenAI.Chat.Completions.ChatCompletion): number {
|
||||||
|
const text = completion.choices[0]?.message?.content || "";
|
||||||
|
|
||||||
|
// Check finish reason - lower confidence if stopped early
|
||||||
|
const finishReason = completion.choices[0]?.finish_reason;
|
||||||
|
if (finishReason === "length" || finishReason === "content_filter") {
|
||||||
|
return 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for uncertainty indicators
|
||||||
|
if (
|
||||||
|
text.includes("I don't know") ||
|
||||||
|
text.includes("I'm not sure") ||
|
||||||
|
text.includes("uncertain") ||
|
||||||
|
text.includes("نمیدانم") ||
|
||||||
|
text.includes("مطمئن نیستم")
|
||||||
|
) {
|
||||||
|
return 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check response length and quality
|
||||||
|
if (text.length < 50) {
|
||||||
|
return 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if response uses provided data
|
||||||
|
if (text.includes("based on") || text.includes("according to") || text.includes("بر اساس") || text.includes("طبق")) {
|
||||||
|
return 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
private estimateTokens(text: string): number {
|
||||||
|
// Rough estimation: 1 token ≈ 4 characters for English text
|
||||||
|
// OpenAI uses similar tokenization to other models
|
||||||
|
return Math.ceil(text.length / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { inject, injectable } from "inversify";
|
||||||
|
import { Socket } from "socket.io";
|
||||||
|
|
||||||
|
import { WebSocketMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { jwtExpiredErr } from "../../../core/app/app.errors";
|
||||||
|
import { Logger } from "../../../core/logging/logger";
|
||||||
|
import { IOCTYPES } from "../../../IOC/ioc.types";
|
||||||
|
import { TokenService } from "../../token/token.service";
|
||||||
|
import { WEBSOCKET_EVENTS } from "../constants/chatbot.constants";
|
||||||
|
import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for WebSocket authentication logic
|
||||||
|
* Follows single responsibility principle and provides reusable authentication methods
|
||||||
|
*/
|
||||||
|
@injectable()
|
||||||
|
export class WebSocketAuthService {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
|
constructor(@inject(IOCTYPES.TokenService) private readonly tokenService: TokenService) {
|
||||||
|
this.logger = new Logger("WebSocketAuthService");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts token from WebSocket client
|
||||||
|
*/
|
||||||
|
private extractTokenFromClient(client: Socket): string | null {
|
||||||
|
// Try to get token from query parameters
|
||||||
|
const token = client.handshake.query.token as string;
|
||||||
|
if (token) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get token from auth header
|
||||||
|
const authHeader = client.handshake.headers.authorization;
|
||||||
|
if (authHeader && typeof authHeader === "string") {
|
||||||
|
const parts = authHeader.split(" ");
|
||||||
|
if (parts.length === 2 && parts[0] === "Bearer") {
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates a WebSocket client and returns the result
|
||||||
|
*
|
||||||
|
* @param client - Socket.IO client to authenticate
|
||||||
|
* @returns Promise with authentication result
|
||||||
|
*/
|
||||||
|
async authenticateClient(client: Socket): Promise<AuthenticationResult> {
|
||||||
|
try {
|
||||||
|
const token = this.extractTokenFromClient(client);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
this.logAuthenticationAttempt(client, false, "No token provided");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: WebSocketMessage.AUTHENTICATION_REQUIRED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this.tokenService.verifyToken(token);
|
||||||
|
|
||||||
|
if (!payload?.sub) {
|
||||||
|
this.logAuthenticationAttempt(client, false, "Invalid token payload");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: WebSocketMessage.INVALID_TOKEN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user data in client (convert sub to id for compatibility)
|
||||||
|
client.data.user = { id: payload.sub, sub: payload.sub };
|
||||||
|
|
||||||
|
this.logAuthenticationAttempt(client, true, undefined, payload.sub);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: { id: payload.sub, sub: payload.sub },
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = this.getAuthErrorMessage(error);
|
||||||
|
this.logAuthenticationAttempt(client, false, errorMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits authentication failure event to client and disconnects
|
||||||
|
*
|
||||||
|
* @param client - Socket.IO client
|
||||||
|
* @param error - Error message to send
|
||||||
|
*/
|
||||||
|
handleAuthenticationFailure(client: Socket, error: string): void {
|
||||||
|
client.emit(WEBSOCKET_EVENTS.UNAUTHORIZED, {
|
||||||
|
message: error,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful disconnect with delay to ensure message is received
|
||||||
|
setTimeout(() => {
|
||||||
|
client.disconnect(true);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits successful authentication event to client
|
||||||
|
*
|
||||||
|
* @param client - Socket.IO client
|
||||||
|
* @param user - Authenticated user payload
|
||||||
|
*/
|
||||||
|
emitAuthenticationSuccess(client: Socket, user: { id: string; sub: string }): void {
|
||||||
|
client.emit(WEBSOCKET_EVENTS.AUTHENTICATED, {
|
||||||
|
message: WebSocketMessage.AUTHENTICATED,
|
||||||
|
userId: user.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates error context object for logging and monitoring
|
||||||
|
*
|
||||||
|
* @param client - Socket.IO client
|
||||||
|
* @param event - Event name (optional)
|
||||||
|
* @param data - Event data (optional)
|
||||||
|
* @returns WebSocket error context
|
||||||
|
*/
|
||||||
|
createErrorContext(client: Socket, event?: WEBSOCKET_EVENTS, data?: any): WebSocketErrorContext {
|
||||||
|
return {
|
||||||
|
clientId: client.id,
|
||||||
|
userId: client.data?.user?.id,
|
||||||
|
sessionId: client.data?.sessionId,
|
||||||
|
event,
|
||||||
|
data,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if client is authenticated
|
||||||
|
*
|
||||||
|
* @param client - Socket.IO client to validate
|
||||||
|
* @returns True if client has valid user data
|
||||||
|
*/
|
||||||
|
isClientAuthenticated(client: Socket): boolean {
|
||||||
|
return !!client.data?.user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets user ID from authenticated client
|
||||||
|
*
|
||||||
|
* @param client - Socket.IO client
|
||||||
|
* @returns User ID or undefined if not authenticated
|
||||||
|
*/
|
||||||
|
getUserId(client: Socket): string | undefined {
|
||||||
|
return client.data?.user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs authentication attempts with structured data
|
||||||
|
*/
|
||||||
|
private logAuthenticationAttempt(client: Socket, success: boolean, error?: string, userId?: string): void {
|
||||||
|
const logData = {
|
||||||
|
clientId: client.id,
|
||||||
|
clientIP: client.handshake.address,
|
||||||
|
success,
|
||||||
|
userId,
|
||||||
|
error,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.logger.info(`WebSocket authentication successful`, logData);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`WebSocket authentication failed`, logData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps JWT errors to user-friendly messages
|
||||||
|
*/
|
||||||
|
private getAuthErrorMessage(error: any): string {
|
||||||
|
if (error instanceof jwtExpiredErr || error?.name === "TokenExpiredError") {
|
||||||
|
return WebSocketMessage.TOKEN_EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error?.name === "JsonWebTokenError") {
|
||||||
|
return WebSocketMessage.INVALID_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error?.name === "NotBeforeError") {
|
||||||
|
return WebSocketMessage.INVALID_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebSocketMessage.INVALID_TOKEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import * as ulidLib from "ulid";
|
||||||
|
|
||||||
|
const CHATBOT_ULID_COOKIE_NAME = "chatbot_session_id";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create ULID for chatbot session from cookies
|
||||||
|
* @param req Express request object
|
||||||
|
* @param res Express response object
|
||||||
|
* @returns ULID string
|
||||||
|
*/
|
||||||
|
export function getOrCreateChatbotUlid(req: Request, res: Response): string {
|
||||||
|
let ulidValue = req.cookies?.[CHATBOT_ULID_COOKIE_NAME];
|
||||||
|
|
||||||
|
if (!ulidValue) {
|
||||||
|
ulidValue = ulidLib.ulid();
|
||||||
|
// Set cookie with 1 year expiration (or adjust as needed)
|
||||||
|
res.cookie(CHATBOT_ULID_COOKIE_NAME, ulidValue, {
|
||||||
|
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ulidValue;
|
||||||
|
}
|
||||||
|
|
||||||
+6
-2
@@ -11,6 +11,7 @@ import { Logger } from "./core/logging/logger";
|
|||||||
import { connectMongo } from "./db/connection";
|
import { connectMongo } from "./db/connection";
|
||||||
import { IOCTYPES } from "./IOC/ioc.types";
|
import { IOCTYPES } from "./IOC/ioc.types";
|
||||||
import { ChatGateway } from "./modules/chat/chat.gateway";
|
import { ChatGateway } from "./modules/chat/chat.gateway";
|
||||||
|
import { ChatbotGateway } from "./modules/chatbot/chatbot.gateway";
|
||||||
import { StartWorker } from "./queues";
|
import { StartWorker } from "./queues";
|
||||||
import { gracefulShutdown } from "./utils/shutdown.utils";
|
import { gracefulShutdown } from "./utils/shutdown.utils";
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ async function bootStrap() {
|
|||||||
const chatGateway = container.get<ChatGateway>(IOCTYPES.ChatGateway);
|
const chatGateway = container.get<ChatGateway>(IOCTYPES.ChatGateway);
|
||||||
const wsServer = chatGateway.initialize(server);
|
const wsServer = chatGateway.initialize(server);
|
||||||
|
|
||||||
|
const chatbotGateway = container.get<ChatbotGateway>(IOCTYPES.ChatbotGateway);
|
||||||
|
const chatbotWsServer = chatbotGateway.initialize(server);
|
||||||
|
|
||||||
const worker = await StartWorker();
|
const worker = await StartWorker();
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -36,9 +40,9 @@ async function bootStrap() {
|
|||||||
logger.info(`swagger documentation is serving on http://localhost:${PORT}/api-docs`);
|
logger.info(`swagger documentation is serving on http://localhost:${PORT}/api-docs`);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => gracefulShutdown("SIGINT", server, wsServer, worker[0], logger));
|
process.on("SIGINT", () => gracefulShutdown("SIGINT", server, wsServer, worker[0], logger, chatbotWsServer));
|
||||||
|
|
||||||
process.on("SIGTERM", () => gracefulShutdown("SIGTERM", server, wsServer, worker[0], logger));
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM", server, wsServer, worker[0], logger, chatbotWsServer));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error starting the server.", error);
|
logger.error("Error starting the server.", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -5,16 +5,29 @@ import { Server as WSServer } from "socket.io";
|
|||||||
|
|
||||||
import { Logger } from "../core/logging/logger";
|
import { Logger } from "../core/logging/logger";
|
||||||
|
|
||||||
export const gracefulShutdown = async (signal: string, server: Server, wsServer: WSServer, worker: Worker, logger: Logger) => {
|
export const gracefulShutdown = async (
|
||||||
|
signal: string,
|
||||||
|
server: Server,
|
||||||
|
wsServer: WSServer,
|
||||||
|
worker: Worker,
|
||||||
|
logger: Logger,
|
||||||
|
chatbotWsServer?: WSServer,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
logger.warn(`${signal} signal received: closing HTTP server and worker...`);
|
logger.warn(`${signal} signal received: closing HTTP server and worker...`);
|
||||||
|
|
||||||
logger.warn("close all socket connection..");
|
logger.warn("close all socket connection..");
|
||||||
wsServer.disconnectSockets();
|
wsServer.disconnectSockets();
|
||||||
|
if (chatbotWsServer) {
|
||||||
|
chatbotWsServer.disconnectSockets();
|
||||||
|
}
|
||||||
logger.warn("all socket connection disconnected");
|
logger.warn("all socket connection disconnected");
|
||||||
|
|
||||||
logger.warn("Closing ws server...");
|
logger.warn("Closing ws server...");
|
||||||
wsServer.close();
|
wsServer.close();
|
||||||
|
if (chatbotWsServer) {
|
||||||
|
chatbotWsServer.close();
|
||||||
|
}
|
||||||
logger.warn("ws server closed");
|
logger.warn("ws server closed");
|
||||||
|
|
||||||
server.close(async () => {
|
server.close(async () => {
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Chatbot Socket.IO Test</title>
|
||||||
|
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
#log {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.log-success { color: green; }
|
||||||
|
.log-error { color: red; }
|
||||||
|
.log-info { color: blue; }
|
||||||
|
input {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 5px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Chatbot Socket.IO Test Client</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Server URL:</label><br>
|
||||||
|
<input type="text" id="serverUrl" value="http://localhost:4000" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Token (optional, for authenticated connection):</label><br>
|
||||||
|
<input type="text" id="token" placeholder="Leave empty for anonymous connection" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<button id="connectBtn" onclick="connect()">Connect</button>
|
||||||
|
<button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
|
||||||
|
<button onclick="createSession()" id="createSessionBtn" disabled>Create Session</button>
|
||||||
|
<button onclick="clearLog()">Clear Log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<label>Session ID:</label><br>
|
||||||
|
<input type="text" id="sessionId" placeholder="Will be set after creating session" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<label>Message:</label><br>
|
||||||
|
<input type="text" id="messageInput" placeholder="Type your message here" />
|
||||||
|
<button onclick="sendMessage()" id="sendMessageBtn" disabled>Send Message</button>
|
||||||
|
<button onclick="sendMessageStream()" id="sendStreamBtn" disabled>Send (Stream)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h3>Event Log:</h3>
|
||||||
|
<div id="log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let socket = null;
|
||||||
|
let currentSessionId = null;
|
||||||
|
|
||||||
|
function log(message, type = 'info') {
|
||||||
|
const logDiv = document.getElementById('log');
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.className = `log-entry log-${type}`;
|
||||||
|
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||||
|
logDiv.appendChild(entry);
|
||||||
|
logDiv.scrollTop = logDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLog() {
|
||||||
|
document.getElementById('log').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const serverUrl = document.getElementById('serverUrl').value;
|
||||||
|
const token = document.getElementById('token').value;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
path: '/ws-chatbot',
|
||||||
|
transports: ['websocket'],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
options.query = { token: token };
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Connecting to ${serverUrl}...`, 'info');
|
||||||
|
socket = io(serverUrl, options);
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
log('✅ Connected! Socket ID: ' + socket.id, 'success');
|
||||||
|
document.getElementById('connectBtn').disabled = true;
|
||||||
|
document.getElementById('disconnectBtn').disabled = false;
|
||||||
|
document.getElementById('createSessionBtn').disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('authenticated', (data) => {
|
||||||
|
log('✅ Authenticated: ' + JSON.stringify(data), 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('unauthorized', (data) => {
|
||||||
|
log('❌ Unauthorized: ' + JSON.stringify(data), 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('session_created', (data) => {
|
||||||
|
log('✅ Session created: ' + JSON.stringify(data, null, 2), 'success');
|
||||||
|
if (data.data && data.data.id) {
|
||||||
|
currentSessionId = data.data.id;
|
||||||
|
document.getElementById('sessionId').value = currentSessionId;
|
||||||
|
document.getElementById('sendMessageBtn').disabled = false;
|
||||||
|
document.getElementById('sendStreamBtn').disabled = false;
|
||||||
|
// Auto-join the session
|
||||||
|
socket.emit('join_chat', currentSessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('chat_joined', (data) => {
|
||||||
|
log('✅ Joined chat: ' + JSON.stringify(data, null, 2), 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('message_received', (data) => {
|
||||||
|
log('📨 Message received: ' + JSON.stringify(data, null, 2), 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('bot_response_start', (data) => {
|
||||||
|
log('🤖 Bot response started...', 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('bot_response_chunk', (data) => {
|
||||||
|
if (data.data && data.data.chunk) {
|
||||||
|
process.stdout = {
|
||||||
|
write: (text) => log('🤖 ' + text, 'info')
|
||||||
|
};
|
||||||
|
// Append chunk to log
|
||||||
|
const logDiv = document.getElementById('log');
|
||||||
|
const lastEntry = logDiv.lastElementChild;
|
||||||
|
if (lastEntry && lastEntry.textContent.includes('🤖')) {
|
||||||
|
lastEntry.textContent += data.data.chunk;
|
||||||
|
} else {
|
||||||
|
log('🤖 ' + data.data.chunk, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('bot_response_end', (data) => {
|
||||||
|
log('✅ Bot response completed: ' + JSON.stringify(data, null, 2), 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('bot_response', (data) => {
|
||||||
|
log('🤖 Bot response: ' + JSON.stringify(data, null, 2), 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (data) => {
|
||||||
|
log('❌ Error: ' + JSON.stringify(data, null, 2), 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('session_error', (data) => {
|
||||||
|
log('❌ Session error: ' + JSON.stringify(data, null, 2), 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('message_error', (data) => {
|
||||||
|
log('❌ Message error: ' + JSON.stringify(data, null, 2), 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
log('❌ Disconnected: ' + reason, 'error');
|
||||||
|
document.getElementById('connectBtn').disabled = false;
|
||||||
|
document.getElementById('disconnectBtn').disabled = true;
|
||||||
|
document.getElementById('createSessionBtn').disabled = true;
|
||||||
|
document.getElementById('sendMessageBtn').disabled = true;
|
||||||
|
document.getElementById('sendStreamBtn').disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (error) => {
|
||||||
|
log('❌ Connection error: ' + error.message, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
socket = null;
|
||||||
|
currentSessionId = null;
|
||||||
|
document.getElementById('sessionId').value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSession() {
|
||||||
|
if (socket && socket.connected) {
|
||||||
|
log('📤 Creating session...', 'info');
|
||||||
|
socket.emit('create_session');
|
||||||
|
} else {
|
||||||
|
log('❌ Not connected!', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
const message = document.getElementById('messageInput').value;
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
log('❌ Not connected!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentSessionId) {
|
||||||
|
log('❌ No session ID! Create a session first.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
log('❌ Message is empty!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('📤 Sending message: ' + message, 'info');
|
||||||
|
socket.emit('send_message', {
|
||||||
|
sessionId: currentSessionId,
|
||||||
|
content: message
|
||||||
|
});
|
||||||
|
document.getElementById('messageInput').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessageStream() {
|
||||||
|
const message = document.getElementById('messageInput').value;
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
log('❌ Not connected!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentSessionId) {
|
||||||
|
log('❌ No session ID! Create a session first.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!message) {
|
||||||
|
log('❌ Message is empty!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('📤 Sending message (stream): ' + message, 'info');
|
||||||
|
socket.emit('send_message_stream', {
|
||||||
|
sessionId: currentSessionId,
|
||||||
|
content: message
|
||||||
|
});
|
||||||
|
document.getElementById('messageInput').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to send message
|
||||||
|
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Test script for ChatbotGateway Socket.IO connection
|
||||||
|
// Run with: node test-chatbot-socket.js
|
||||||
|
|
||||||
|
const io = require("socket.io-client");
|
||||||
|
|
||||||
|
// Connect to the chatbot gateway
|
||||||
|
// The path option matches the path configured in ChatbotGateway
|
||||||
|
const socket = io("http://localhost:4000", {
|
||||||
|
path: "/ws-chatbot",
|
||||||
|
transports: ["websocket"],
|
||||||
|
// Optional: Add token for authenticated connection
|
||||||
|
// query: {
|
||||||
|
// token: "your-jwt-token-here"
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("✅ Connected to ChatbotGateway!");
|
||||||
|
console.log("Socket ID:", socket.id);
|
||||||
|
|
||||||
|
// Test: Create a session
|
||||||
|
console.log("\n📤 Creating session...");
|
||||||
|
socket.emit("create_session");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("authenticated", (data) => {
|
||||||
|
console.log("✅ Authenticated:", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("unauthorized", (data) => {
|
||||||
|
console.log("❌ Unauthorized:", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("session_created", (data) => {
|
||||||
|
console.log("✅ Session created:", JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
// Test: Join the session
|
||||||
|
if (data.data && data.data.id) {
|
||||||
|
console.log("\n📤 Joining session:", data.data.id);
|
||||||
|
socket.emit("join_chat", data.data.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("chat_joined", (data) => {
|
||||||
|
console.log("✅ Joined chat:", JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
// Test: Send a message
|
||||||
|
if (data.data && data.data.id) {
|
||||||
|
console.log("\n📤 Sending test message...");
|
||||||
|
socket.emit("send_message", {
|
||||||
|
sessionId: data.data.id,
|
||||||
|
content: "Hello, chatbot!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("message_received", (data) => {
|
||||||
|
console.log("📨 Message received:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("bot_response_start", (data) => {
|
||||||
|
console.log("🤖 Bot response started:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("bot_response_chunk", (data) => {
|
||||||
|
process.stdout.write(data.data?.chunk || "");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("bot_response_end", (data) => {
|
||||||
|
console.log("\n✅ Bot response completed:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("bot_response", (data) => {
|
||||||
|
console.log("🤖 Bot response:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (data) => {
|
||||||
|
console.log("❌ Error:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("session_error", (data) => {
|
||||||
|
console.log("❌ Session error:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("message_error", (data) => {
|
||||||
|
console.log("❌ Message error:", JSON.stringify(data, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", (reason) => {
|
||||||
|
console.log("❌ Disconnected:", reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (error) => {
|
||||||
|
console.log("❌ Connection error:", error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the script running
|
||||||
|
process.stdin.resume();
|
||||||
|
|
||||||
Reference in New Issue
Block a user