чистка
This commit is contained in:
222
package-lock.json
generated
222
package-lock.json
generated
@@ -9,10 +9,9 @@
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.3.41",
|
||||
"@langchain/core": "^0.3.46",
|
||||
"@langchain/langgraph": "^0.2.65",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@langchain/community": "^0.3.56",
|
||||
"@langchain/core": "^0.3.77",
|
||||
"@langchain/langgraph": "^0.4.9",
|
||||
"ai": "^4.1.13",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.0",
|
||||
@@ -25,15 +24,15 @@
|
||||
"express": "5.0.1",
|
||||
"express-jwt": "^8.5.1",
|
||||
"express-session": "^1.18.1",
|
||||
"gigachat": "^0.0.14",
|
||||
"gigachat": "^0.0.16",
|
||||
"jsdom": "^25.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"langchain": "^0.3.7",
|
||||
"langchain-gigachat": "^0.0.11",
|
||||
"mongodb": "^6.12.0",
|
||||
"mongoose": "^8.9.2",
|
||||
"langchain": "^0.3.34",
|
||||
"langchain-gigachat": "^0.0.14",
|
||||
"mongodb": "^6.20.0",
|
||||
"mongoose": "^8.18.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pbkdf2-password": "^1.2.1",
|
||||
"rotating-file-stream": "^3.2.5",
|
||||
@@ -1928,19 +1927,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/community": {
|
||||
"version": "0.3.46",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.46.tgz",
|
||||
"integrity": "sha512-loix9LkoNcn1gQlVCopmrJW9TmgZb+YpZw7nkFzXT6ozR8ZDh1XlFq1ymR5gTFtdNzF0neK2oJtE9iEl1lm7Dw==",
|
||||
"version": "0.3.56",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.56.tgz",
|
||||
"integrity": "sha512-lDjUnRfHAX7aMXyEB2EWbe5qOmdQdz8n+0CNQ4ExpLy3NOFQhEVkWclhsucaX04zh0r/VH5Pkk9djpnhPBDH7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/openai": ">=0.2.0 <0.6.0",
|
||||
"@langchain/openai": ">=0.2.0 <0.7.0",
|
||||
"@langchain/weaviate": "^0.2.0",
|
||||
"binary-extensions": "^2.2.0",
|
||||
"expr-eval": "^2.0.2",
|
||||
"flat": "^5.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0",
|
||||
"langsmith": "^0.3.29",
|
||||
"langsmith": "^0.3.67",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
@@ -1975,10 +1974,10 @@
|
||||
"@google-ai/generativelanguage": "*",
|
||||
"@google-cloud/storage": "^6.10.1 || ^7.7.0",
|
||||
"@gradientai/nodejs-sdk": "^1.2.0",
|
||||
"@huggingface/inference": "^2.6.4",
|
||||
"@huggingface/transformers": "^3.2.3",
|
||||
"@huggingface/inference": "^4.0.5",
|
||||
"@huggingface/transformers": "^3.5.2",
|
||||
"@ibm-cloud/watsonx-ai": "*",
|
||||
"@lancedb/lancedb": "^0.12.0",
|
||||
"@lancedb/lancedb": "^0.19.1",
|
||||
"@langchain/core": ">=0.3.58 <0.4.0",
|
||||
"@layerup/layerup-security": "^1.5.12",
|
||||
"@libsql/client": "^0.14.0",
|
||||
@@ -1991,7 +1990,7 @@
|
||||
"@pinecone-database/pinecone": "*",
|
||||
"@planetscale/database": "^1.8.0",
|
||||
"@premai/prem-sdk": "^0.3.25",
|
||||
"@qdrant/js-client-rest": "^1.8.2",
|
||||
"@qdrant/js-client-rest": "^1.15.0",
|
||||
"@raycast/api": "^1.55.2",
|
||||
"@rockset/client": "^0.9.1",
|
||||
"@smithy/eventstream-codec": "^2.0.5",
|
||||
@@ -2027,11 +2026,10 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3-dsv": "^2.0.0",
|
||||
"discord.js": "^14.14.1",
|
||||
"dria": "^0.0.3",
|
||||
"duck-duck-scrape": "^2.2.5",
|
||||
"epub2": "^3.0.1",
|
||||
"fast-xml-parser": "*",
|
||||
"firebase-admin": "^11.9.0 || ^12.0.0",
|
||||
"firebase-admin": "^11.9.0 || ^12.0.0 || ^13.0.0",
|
||||
"google-auth-library": "*",
|
||||
"googleapis": "*",
|
||||
"hnswlib-node": "^3.0.0",
|
||||
@@ -2049,7 +2047,7 @@
|
||||
"mammoth": "^1.6.0",
|
||||
"mariadb": "^3.4.0",
|
||||
"mem0ai": "^2.1.8",
|
||||
"mongodb": ">=5.2.0",
|
||||
"mongodb": "^6.17.0",
|
||||
"mysql2": "^3.9.8",
|
||||
"neo4j-driver": "*",
|
||||
"notion-to-md": "^3.1.0",
|
||||
@@ -2309,9 +2307,6 @@
|
||||
"discord.js": {
|
||||
"optional": true
|
||||
},
|
||||
"dria": {
|
||||
"optional": true
|
||||
},
|
||||
"duck-duck-scrape": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -2466,9 +2461,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "0.3.58",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.58.tgz",
|
||||
"integrity": "sha512-HLkOtVofgBHefaUae/+2fLNkpMLzEjHSavTmUF0YC7bDa5NPIZGlP80CGrSFXAeJ+WCPd8rIK8K/p6AW94inUQ==",
|
||||
"version": "0.3.77",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.77.tgz",
|
||||
"integrity": "sha512-aqXHea9xfpVn6VoCq9pjujwFqrh3vw3Fgm9KFUZJ1cF7Bx5HI62DvQPw8LlRB3NB4dhwBBA1ldAVkkkd1du8nA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
@@ -2476,7 +2471,7 @@
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": "^0.3.29",
|
||||
"langsmith": "^0.3.67",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "4",
|
||||
@@ -2526,21 +2521,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph": {
|
||||
"version": "0.2.74",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz",
|
||||
"integrity": "sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==",
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.4.9.tgz",
|
||||
"integrity": "sha512-+rcdTGi4Ium4X/VtIX3Zw4RhxEkYWpwUyz806V6rffjHOAMamg6/WZDxpJbrP33RV/wJG1GH12Z29oX3Pqq3Aw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph-checkpoint": "~0.0.17",
|
||||
"@langchain/langgraph-sdk": "~0.0.32",
|
||||
"@langchain/langgraph-checkpoint": "^0.1.1",
|
||||
"@langchain/langgraph-sdk": "~0.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0",
|
||||
"@langchain/core": ">=0.3.58 < 0.4.0",
|
||||
"zod-to-json-schema": "^3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -2550,9 +2545,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz",
|
||||
"integrity": "sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==",
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.1.1.tgz",
|
||||
"integrity": "sha512-h2bP0RUikQZu0Um1ZUPErQLXyhzroJqKRbRcxYRTAh49oNlsfeq4A3K4YEDRbGGuyPZI/Jiqwhks1wZwY73AZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^10.0.0"
|
||||
@@ -2561,7 +2556,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.31 <0.4.0"
|
||||
"@langchain/core": ">=0.2.31 <0.4.0 || ^1.0.0-alpha"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": {
|
||||
@@ -2578,9 +2573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk": {
|
||||
"version": "0.0.84",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.84.tgz",
|
||||
"integrity": "sha512-l0PFQyJ+6m6aclORNPPWlcRwgKcXVXsPaJCbCUYFABR3yf4cOpsjhUNR0cJ7+2cS400oieHjGRdGGyO/hbSjhg==",
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.1.6.tgz",
|
||||
"integrity": "sha512-PeXxfo4ls8yql6YdW8qjnZgp1giy7oqJiGjy4j2OSJ7lpkir8n62YpvADDByEh9sPzGLJYh92ZUAh0GNfQ18vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
@@ -2589,8 +2584,9 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.31 <0.4.0",
|
||||
"react": "^18 || ^19"
|
||||
"@langchain/core": ">=0.2.31 <0.4.0 || ^1.0.0-alpha",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@langchain/core": {
|
||||
@@ -2598,6 +2594,9 @@
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2737,9 +2736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
|
||||
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
|
||||
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
@@ -2871,6 +2870,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz",
|
||||
"integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -2880,6 +2881,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
|
||||
"integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -2889,6 +2892,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
@@ -2901,6 +2906,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
|
||||
"integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -2910,6 +2917,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
|
||||
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14",
|
||||
"@types/phoenix": "^1.5.4",
|
||||
@@ -2922,6 +2931,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
|
||||
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -2931,6 +2942,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz",
|
||||
"integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.69.1",
|
||||
"@supabase/functions-js": "2.4.4",
|
||||
@@ -3133,7 +3146,9 @@
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
@@ -3195,6 +3210,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -3802,9 +3819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz",
|
||||
"integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==",
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
|
||||
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
@@ -5760,9 +5777,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gigachat": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.14.tgz",
|
||||
"integrity": "sha512-BwXDecDxF6aKJT+juuoATrBnFLDBg5Vho1dxYRsgM18zgZ55q5SwNiOgC05/J7rhGY66Pj6Wsnvk3FC6K4IMQw==",
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.16.tgz",
|
||||
"integrity": "sha512-37MFTFltKGDE1EDW6y87BxzU5orIU3fpLDqAMHCNdV8JUL2oNbHMe6CACWWqUh7HLaztwkysRP8nJxBYBms1gg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
@@ -7389,17 +7406,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/langchain": {
|
||||
"version": "0.3.28",
|
||||
"resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.28.tgz",
|
||||
"integrity": "sha512-h4GGlBJNGU/Sj2PipW9kL+ewj7To3c+SnnNKH3HZaVHEqGPMHVB96T1lLjtCLcZCyUfabMr/zFIkLNI4War+Xg==",
|
||||
"version": "0.3.34",
|
||||
"resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.34.tgz",
|
||||
"integrity": "sha512-OADHLQYRX+36EqQBxIoryCdMKfHex32cJBSWveadIIeRhygqivacIIDNwVjX51Y++c80JIdR0jaQHWn2r3H1iA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/openai": ">=0.1.0 <0.6.0",
|
||||
"@langchain/openai": ">=0.1.0 <0.7.0",
|
||||
"@langchain/textsplitters": ">=0.0.0 <0.2.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpointer": "^5.0.1",
|
||||
"langsmith": "^0.3.29",
|
||||
"langsmith": "^0.3.67",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-retry": "4",
|
||||
"uuid": "^10.0.0",
|
||||
@@ -7484,12 +7501,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/langchain-gigachat": {
|
||||
"version": "0.0.11",
|
||||
"resolved": "https://registry.npmjs.org/langchain-gigachat/-/langchain-gigachat-0.0.11.tgz",
|
||||
"integrity": "sha512-2hYES1Dt0U/p/h+F+/1lDfmaYTWQyuHG5KAAIQGYygursAUGDDoyKQlGywbJ4JgmENy4u5fv7keVC9+k0X8tbQ==",
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/langchain-gigachat/-/langchain-gigachat-0.0.14.tgz",
|
||||
"integrity": "sha512-8jnHMZI1QqAs98iTdldouT1chiFRTtEnxXHFiQl8th7u/B6Eot0OJfMT5iviCFO6/pMNxYgq0Fzzr29ndaJyEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"gigachat": "^0.0.14",
|
||||
"gigachat": "^0.0.15",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
@@ -7501,6 +7518,16 @@
|
||||
"@langchain/core": ">=0.2.21 <0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/langchain-gigachat/node_modules/gigachat": {
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.15.tgz",
|
||||
"integrity": "sha512-4hAf/obnzwW4xp+AOP6Zv81F3Dr9QcsEjVOGTdY4aRWphzgV8YVZ134huqQfA/LQCuoD9UMmlt3nfix6exgjYg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"uuid": "^11.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/langchain/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -7515,9 +7542,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.31.tgz",
|
||||
"integrity": "sha512-9lwuLZuN3tXFYQ6eMg0rmbBw7oxQo4bu1NYeylbjz27bOdG1XB9XNoxaiIArkK4ciLdOIOhPMBXP4bkvZOgHRw==",
|
||||
"version": "0.3.69",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.69.tgz",
|
||||
"integrity": "sha512-YKzu92YAP2o+d+1VmR38xqFX0RIRLKYj1IqdflVEY83X0FoiVlrWO3xDLXgnu7vhZ2N2M6jx8VO9fVF8yy9gHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
@@ -7529,9 +7556,21 @@
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -7862,14 +7901,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz",
|
||||
"integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==",
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
|
||||
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.1",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
"@mongodb-js/saslprep": "^1.3.0",
|
||||
"bson": "^6.10.4",
|
||||
"mongodb-connection-string-url": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
@@ -7880,7 +7919,7 @@
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"snappy": "^7.3.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -7952,14 +7991,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.9.5",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz",
|
||||
"integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==",
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.2.tgz",
|
||||
"integrity": "sha512-gA6GFlshOHUdNyw9OQTmMLSGzVOPbcbjaSZ1dvR5iMp668N2UUznTuzgTY6V6Q41VtBc4kmL/qqML1RNgXB5Fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.1",
|
||||
"bson": "^6.10.4",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.12.0",
|
||||
"mongodb": "~6.18.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
@@ -7987,13 +8026,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz",
|
||||
"integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.1",
|
||||
"bson": "^6.10.4",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8039,16 +8078,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-headers": "~1.0.2"
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -8066,6 +8105,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/morgan/node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/mpath": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||
|
||||
19
package.json
19
package.json
@@ -21,10 +21,9 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@langchain/community": "^0.3.41",
|
||||
"@langchain/core": "^0.3.46",
|
||||
"@langchain/langgraph": "^0.2.65",
|
||||
"@langchain/community": "^0.3.56",
|
||||
"@langchain/core": "^0.3.77",
|
||||
"@langchain/langgraph": "^0.4.9",
|
||||
"ai": "^4.1.13",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.0",
|
||||
@@ -37,15 +36,15 @@
|
||||
"express": "5.0.1",
|
||||
"express-jwt": "^8.5.1",
|
||||
"express-session": "^1.18.1",
|
||||
"gigachat": "^0.0.14",
|
||||
"gigachat": "^0.0.16",
|
||||
"jsdom": "^25.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"langchain": "^0.3.7",
|
||||
"langchain-gigachat": "^0.0.11",
|
||||
"mongodb": "^6.12.0",
|
||||
"mongoose": "^8.9.2",
|
||||
"langchain": "^0.3.34",
|
||||
"langchain-gigachat": "^0.0.14",
|
||||
"mongodb": "^6.20.0",
|
||||
"mongoose": "^8.18.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pbkdf2-password": "^1.2.1",
|
||||
"rotating-file-stream": "^3.2.5",
|
||||
|
||||
@@ -20,9 +20,7 @@ import gamehubRouter from './routers/gamehub'
|
||||
import escRouter from './routers/esc'
|
||||
import connectmeRouter from './routers/connectme'
|
||||
import questioneerRouter from './routers/questioneer'
|
||||
import backNewRouter from './routers/back-new/app'
|
||||
import { setIo } from './io'
|
||||
const { createChatPollingRouter } = require('./routers/kfu-m-24-1/sber_mobile/polling-chat')
|
||||
|
||||
export const app = express()
|
||||
|
||||
@@ -90,18 +88,11 @@ const initServer = async () => {
|
||||
)
|
||||
app.use(root)
|
||||
|
||||
// Инициализация Polling для чата (после настройки middleware)
|
||||
const { router: chatPollingRouter, chatHandler } = createChatPollingRouter(express)
|
||||
|
||||
|
||||
/**
|
||||
* Добавляйте сюда свои routers.
|
||||
*/
|
||||
app.use("/kfu-m-24-1", kfuM241Router)
|
||||
|
||||
// Добавляем Polling роутер для чата
|
||||
app.use("/kfu-m-24-1/sber_mobile", chatPollingRouter)
|
||||
|
||||
app.use("/epja-2024-1", epja20241Router)
|
||||
app.use("/v1/todo", todoRouter)
|
||||
app.use("/dogsitters-finder", dogsittersFinderRouter)
|
||||
@@ -114,7 +105,6 @@ const initServer = async () => {
|
||||
app.use("/esc", escRouter)
|
||||
app.use('/connectme', connectmeRouter)
|
||||
app.use('/questioneer', questioneerRouter)
|
||||
app.use('/back-new', backNewRouter)
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
GIGACHAT_API_KEY=78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974
|
||||
GIGACHAT_SCOPE=GIGACHAT_API_PERS
|
||||
2
server/routers/back-new/.gitignore
vendored
2
server/routers/back-new/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
.env
|
||||
@@ -1,21 +0,0 @@
|
||||
# back-new
|
||||
|
||||
非Python实现的后端(Node.js + Express)
|
||||
|
||||
## 启动方法
|
||||
|
||||
1. 安装依赖:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. 启动服务:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
默认端口:`3002`
|
||||
|
||||
## 支持接口
|
||||
- POST `/api/auth/login` 用户登录
|
||||
- POST `/api/auth/register` 用户注册
|
||||
- GET `/gigachat/prompt?prompt=xxx` 生成图片(返回模拟图片链接)
|
||||
@@ -1,22 +0,0 @@
|
||||
const express = require('express');
|
||||
const featuresConfig = require('./features.config');
|
||||
const imageRoutes = require('./features/image/image.routes');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 动态加载路由
|
||||
if (featuresConfig.auth) {
|
||||
router.use('/auth', require('./features/auth/auth.routes'));
|
||||
}
|
||||
if (featuresConfig.user) {
|
||||
router.use('/user', require('./features/user/user.routes'));
|
||||
}
|
||||
if (featuresConfig.image) {
|
||||
router.use('/image', imageRoutes);
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.json({ message: 'API root' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
auth: true,
|
||||
user: true,
|
||||
image: true, // 关闭为 false
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
const usersDb = require('../../shared/usersDb');
|
||||
const makeLinks = require('../../shared/hateoas');
|
||||
|
||||
exports.login = (req, res) => {
|
||||
const { username, password, email } = req.body;
|
||||
const user = usersDb.findUser(username, email, password);
|
||||
if (user) {
|
||||
res.json({
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
},
|
||||
token: 'token-' + user.id,
|
||||
message: 'Login successful'
|
||||
},
|
||||
_links: makeLinks('/api/auth', {
|
||||
self: '/login',
|
||||
profile: '/profile/',
|
||||
logout: '/logout'
|
||||
}),
|
||||
_meta: {}
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.register = (req, res) => {
|
||||
const { username, password, email, firstName, lastName } = req.body;
|
||||
if (usersDb.exists(username, email)) {
|
||||
return res.status(409).json({ error: 'User already exists' });
|
||||
}
|
||||
const newUser = usersDb.addUser({ username, password, email, firstName, lastName });
|
||||
res.json({
|
||||
data: {
|
||||
user: {
|
||||
id: newUser.id,
|
||||
username,
|
||||
email,
|
||||
firstName,
|
||||
lastName
|
||||
},
|
||||
token: 'token-' + newUser.id,
|
||||
message: 'Register successful'
|
||||
},
|
||||
_links: makeLinks('/api/auth', {
|
||||
self: '/register',
|
||||
login: '/login',
|
||||
profile: '/profile/'
|
||||
}),
|
||||
_meta: {}
|
||||
});
|
||||
};
|
||||
|
||||
exports.profile = (req, res) => {
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth || !auth.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
const token = auth.replace('Bearer ', '');
|
||||
const id = parseInt(token.replace('token-', ''));
|
||||
const user = usersDb.findById(id);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
res.json({
|
||||
data: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
},
|
||||
_links: makeLinks('/api/auth', {
|
||||
self: '/profile/',
|
||||
logout: '/logout'
|
||||
}),
|
||||
_meta: {}
|
||||
});
|
||||
};
|
||||
|
||||
exports.logout = (req, res) => {
|
||||
res.json({
|
||||
message: 'Logout successful',
|
||||
_links: makeLinks('/api/auth', {
|
||||
self: '/logout',
|
||||
login: '/login'
|
||||
}),
|
||||
_meta: {}
|
||||
});
|
||||
};
|
||||
|
||||
exports.updateProfile = (req, res) => {
|
||||
const userId = req.user?.id || req.body.id; // 这里假设有用户认证中间件,否则用body.id
|
||||
if (!userId) return res.status(401).json({ error: 'Unauthorized' });
|
||||
const { firstName, lastName, bio, location, website, email, username, password } = req.body;
|
||||
const updated = require('../../shared/usersDb').updateUser(userId, { firstName, lastName, bio, location, website, email, username, password });
|
||||
if (!updated) return res.status(404).json({ error: 'User not found' });
|
||||
res.json({ success: true, user: updated });
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('./auth.controller');
|
||||
|
||||
router.post('/login', ctrl.login);
|
||||
router.post('/register', ctrl.register);
|
||||
router.get('/profile/', ctrl.profile);
|
||||
router.post('/logout', ctrl.logout);
|
||||
router.put('/profile/', ctrl.updateProfile);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,157 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const makeLinks = require('../../shared/hateoas');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const qs = require('qs');
|
||||
require('dotenv').config();
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
// 获取GigaChat access_token,严格按官方文档
|
||||
async function getGigaChatToken() {
|
||||
const apiKey = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974';
|
||||
const scope = process.env.GIGACHAT_SCOPE || 'GIGACHAT_API_PERS';
|
||||
if (!apiKey) throw new Error('GIGACHAT_API_KEY 未配置');
|
||||
|
||||
const rqUID = uuidv4();
|
||||
const auth = Buffer.from(apiKey.trim()).toString('base64');
|
||||
|
||||
try {
|
||||
const resp = await axios.post(
|
||||
'https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
|
||||
new URLSearchParams({ scope }),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'RqUID': rqUID,
|
||||
'Authorization': `Basic ${auth}`,
|
||||
},
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
if (!resp.data.access_token) {
|
||||
console.error('GigaChat token响应异常:', resp.data);
|
||||
throw new Error('GigaChat token响应异常');
|
||||
}
|
||||
return resp.data.access_token;
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
console.error('获取access_token失败:', err.response.status, err.response.data);
|
||||
} else {
|
||||
console.error('获取access_token异常:', err.message);
|
||||
}
|
||||
throw new Error('获取access_token失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 调用chat生成图片描述
|
||||
async function fetchChatContent(accessToken, prompt) {
|
||||
try {
|
||||
const chatResp = await axios.post(
|
||||
'https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
|
||||
{
|
||||
model: "GigaChat",
|
||||
messages: [
|
||||
{ role: "system", content: "Ты — Василий Кандинский" },
|
||||
{ role: "user", content: prompt }
|
||||
],
|
||||
stream: false,
|
||||
function_call: 'auto'
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'RqUID': uuidv4(),
|
||||
}
|
||||
}
|
||||
);
|
||||
const content = chatResp.data.choices[0].message.content;
|
||||
console.log('GigaChat返回内容:', content);
|
||||
return content;
|
||||
} catch (err) {
|
||||
console.error('AI生成图片出错: chat接口失败');
|
||||
if (err.response) {
|
||||
console.error('status:', err.response.status);
|
||||
console.error('headers:', err.response.headers);
|
||||
console.error('data:', err.response.data);
|
||||
console.error('config:', err.config);
|
||||
} else {
|
||||
console.error('AI生成图片出错:', err.message);
|
||||
}
|
||||
throw new Error('chat接口失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图片内容
|
||||
async function fetchImageContent(accessToken, imageId) {
|
||||
try {
|
||||
const imageResp = await axios.get(
|
||||
`https://gigachat.devices.sberbank.ru/api/v1/files/${imageId}/content`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'RqUID': uuidv4(),
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
);
|
||||
return imageResp.data;
|
||||
} catch (err) {
|
||||
console.error('AI生成图片出错: 获取图片内容失败');
|
||||
if (err.response) {
|
||||
console.error('status:', err.response.status);
|
||||
console.error('headers:', err.response.headers);
|
||||
console.error('data:', err.response.data);
|
||||
console.error('config:', err.config);
|
||||
} else {
|
||||
console.error('AI生成图片出错:', err.message);
|
||||
}
|
||||
throw new Error('获取图片内容失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:异步重试
|
||||
async function retryAsync(fn, times = 3, delay = 800) {
|
||||
let lastErr;
|
||||
for (let i = 0; i < times; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
if (i < times - 1 && delay) await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
exports.generate = async (req, res) => {
|
||||
const { prompt } = req.query;
|
||||
if (!prompt) {
|
||||
return res.status(400).json({ error: 'Prompt parameter is required' });
|
||||
}
|
||||
let accessToken;
|
||||
try {
|
||||
accessToken = await getGigaChatToken();
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
try {
|
||||
// 1. 重试获取图片描述内容
|
||||
const content = await retryAsync(() => fetchChatContent(accessToken, prompt), 3, 800);
|
||||
// 升级正则,兼容更多图片标签格式
|
||||
const match = content.match(/<img[^>]+src=['"]([^'"]+)['"]/);
|
||||
if (!match) {
|
||||
console.error('AI生成图片出错: GigaChat未返回图片标签');
|
||||
return res.status(500).json({ error: 'No image generated' });
|
||||
}
|
||||
const imageId = match[1];
|
||||
// 2. 重试获取图片内容
|
||||
const imageData = await retryAsync(() => fetchImageContent(accessToken, imageId), 3, 800);
|
||||
res.set('Content-Type', 'image/jpeg');
|
||||
res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' })));
|
||||
res.send(imageData);
|
||||
} catch (err) {
|
||||
console.error('AI生成图片出错: 未知错误', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('./image.controller');
|
||||
|
||||
router.get('/prompt', ctrl.generate);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,12 +0,0 @@
|
||||
const usersDb = require('../../shared/usersDb');
|
||||
const makeLinks = require('../../shared/hateoas');
|
||||
|
||||
exports.list = (req, res) => {
|
||||
res.json({
|
||||
data: usersDb.getAll(),
|
||||
_links: makeLinks('/api/user', {
|
||||
self: '/list',
|
||||
}),
|
||||
_meta: {}
|
||||
});
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('./user.controller');
|
||||
|
||||
router.get('/list', ctrl.list);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,11 +0,0 @@
|
||||
// 简单token认证中间件,支持token-3格式
|
||||
module.exports = function (req, res, next) {
|
||||
const auth = req.headers.authorization;
|
||||
if (auth && auth.startsWith('Bearer token-')) {
|
||||
const id = parseInt(auth.replace('Bearer token-', ''));
|
||||
if (!isNaN(id)) {
|
||||
req.user = { id };
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
1024
server/routers/back-new/package-lock.json
generated
1024
server/routers/back-new/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "back-new",
|
||||
"version": "1.0.0",
|
||||
"description": "非Python实现的后端,兼容前端接口",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.0.0",
|
||||
"express": "^4.21.2",
|
||||
"qs": "^6.14.0",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
process.env.GIGACHAT_API_KEY = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974';
|
||||
process.env.GIGACHAT_SCOPE = 'GIGACHAT_API_PERS';
|
||||
require('dotenv').config();
|
||||
console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY);
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
const router = require('./app');
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());process.env.GIGACHAT_API_KEY = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974';
|
||||
process.env.GIGACHAT_SCOPE = 'GIGACHAT_API_PERS';
|
||||
require('dotenv').config();
|
||||
console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY);
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const authMiddleware = require('./middleware/auth');
|
||||
|
||||
const app = express();
|
||||
const router = require('./app');
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(authMiddleware);
|
||||
|
||||
// 路由前缀要和前端请求一致
|
||||
app.use('/ms/back-new', router);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
// 路由前缀要和前端请求一致
|
||||
app.use('/ms/back-new', router);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
function makeLinks(base, links) {
|
||||
const result = {};
|
||||
for (const [rel, path] of Object.entries(links)) {
|
||||
result[rel] = { href: base + path };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
module.exports = makeLinks;
|
||||
@@ -1,28 +0,0 @@
|
||||
let users = [
|
||||
{ id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User', bio: '', location: '', website: '' }
|
||||
];
|
||||
let nextId = 2;
|
||||
|
||||
exports.findUser = (username, email, password) =>
|
||||
users.find(u => (u.username === username || u.email === email) && u.password === password);
|
||||
|
||||
exports.findById = (id) => users.find(u => u.id === id);
|
||||
|
||||
exports.addUser = ({ username, password, email, firstName, lastName, bio = '', location = '', website = '' }) => {
|
||||
const newUser = { id: nextId++, username, password, email, firstName, lastName, bio, location, website };
|
||||
users.push(newUser);
|
||||
return newUser;
|
||||
};
|
||||
|
||||
exports.exists = (username, email) =>
|
||||
users.some(u => u.username === username || u.email === email);
|
||||
|
||||
exports.getAll = () => users;
|
||||
|
||||
// 新增:更新用户信息
|
||||
exports.updateUser = (id, update) => {
|
||||
const user = users.find(u => u.id === id);
|
||||
if (!user) return null;
|
||||
Object.assign(user, update);
|
||||
return user;
|
||||
};
|
||||
@@ -4,7 +4,6 @@ const router = Router()
|
||||
router.use('/eng-it-lean', require('./eng-it-lean/index'))
|
||||
router.use('/sberhubproject', require('./sberhubproject/index'))
|
||||
router.use('/sber_web', require('./sber_web/index'))
|
||||
router.use('/sber_mobile', require('./sber_mobile/index'))
|
||||
|
||||
module.exports = router
|
||||
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
-- Расширение для генерации UUID
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- 1. Управляющие компании
|
||||
CREATE TABLE management_companies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
logo_url TEXT,
|
||||
contact_phone TEXT NOT NULL,
|
||||
email TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 2. Жилые дома
|
||||
CREATE TABLE buildings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
management_company_id UUID NOT NULL REFERENCES management_companies(id),
|
||||
name TEXT,
|
||||
address TEXT NOT NULL,
|
||||
floors INTEGER,
|
||||
entrances INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 3. Профили пользователей
|
||||
CREATE TABLE user_profiles (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id),
|
||||
full_name TEXT,
|
||||
avatar_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 4. Квартиры
|
||||
CREATE TABLE apartments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
building_id UUID NOT NULL REFERENCES buildings(id),
|
||||
number TEXT NOT NULL,
|
||||
area DECIMAL(10, 2),
|
||||
floor INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 5. Связь пользователей с квартирами
|
||||
CREATE TABLE apartment_residents (
|
||||
apartment_id UUID NOT NULL REFERENCES apartments(id),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
is_owner BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (apartment_id, user_id)
|
||||
);
|
||||
|
||||
-- 6. Сервисы УК
|
||||
CREATE TABLE management_services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
management_company_id UUID NOT NULL REFERENCES management_companies(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL,
|
||||
base_price DECIMAL(10, 2),
|
||||
image_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 7. Связь сервисов УК с домами
|
||||
CREATE TABLE building_management_services (
|
||||
building_id UUID NOT NULL REFERENCES buildings(id),
|
||||
service_id UUID NOT NULL REFERENCES management_services(id),
|
||||
custom_price DECIMAL(10, 2),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (building_id, service_id)
|
||||
);
|
||||
|
||||
-- 9. Дополнительные сервисы
|
||||
CREATE TABLE additional_services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL,
|
||||
price DECIMAL(10, 2),
|
||||
image_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 10. Инициативы
|
||||
CREATE TABLE initiatives (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
building_id UUID NOT NULL REFERENCES buildings(id),
|
||||
creator_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (
|
||||
status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected')
|
||||
),
|
||||
target_amount DECIMAL(10, 2),
|
||||
current_amount DECIMAL(10, 2) DEFAULT 0,
|
||||
image_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 11. Голосования
|
||||
CREATE TABLE votes (
|
||||
initiative_id UUID NOT NULL REFERENCES initiatives(id),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (initiative_id, user_id)
|
||||
);
|
||||
|
||||
-- 12. Чат
|
||||
CREATE TABLE chats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
building_id UUID NOT NULL REFERENCES buildings(id),
|
||||
name TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 13. Сообщения
|
||||
CREATE TABLE messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
chat_id UUID NOT NULL REFERENCES chats(id),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
text TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 14. Камеры
|
||||
CREATE TABLE cameras (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
building_id UUID NOT NULL REFERENCES buildings(id),
|
||||
location TEXT NOT NULL,
|
||||
stream_url TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 15. Платежки по квартире (ЖКХ, Интернет и т.д.)
|
||||
CREATE TABLE payment_services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
apartment_id UUID NOT NULL REFERENCES apartments(id),
|
||||
name TEXT NOT NULL, -- Например, "ЖКХ", "Интернет"
|
||||
icon TEXT, -- Можно хранить название иконки или url
|
||||
amount DECIMAL(10, 2) NOT NULL, -- Общая сумма по платежке
|
||||
is_paid BOOLEAN DEFAULT FALSE, -- Оплачен ли весь агрегатор
|
||||
payment_method TEXT CHECK (payment_method IN ('card', 'sber')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 16. Детализация по платежке (например, отопление, вода и т.д.)
|
||||
CREATE TABLE payment_service_details (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
payment_service_id UUID NOT NULL REFERENCES payment_services(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL, -- Например, "Отопление"
|
||||
amount DECIMAL(10, 2) NOT NULL, -- Сумма по детализации
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 17. Заявки
|
||||
CREATE TABLE tickets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
apartment_id UUID NOT NULL REFERENCES apartments(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')),
|
||||
category TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 18. Сообщения в службу поддержки
|
||||
CREATE TABLE support (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
message TEXT NOT NULL,
|
||||
is_from_user BOOLEAN NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Индексы
|
||||
CREATE INDEX idx_buildings_management_company ON buildings(management_company_id);
|
||||
CREATE INDEX idx_management_services_company ON management_services(management_company_id);
|
||||
CREATE INDEX idx_building_services_building ON building_management_services(building_id);
|
||||
CREATE INDEX idx_initiatives_building ON initiatives(building_id);
|
||||
CREATE INDEX idx_votes_initiative ON votes(initiative_id);
|
||||
CREATE INDEX idx_messages_chat ON messages(chat_id);
|
||||
CREATE INDEX idx_cameras_building ON cameras(building_id);
|
||||
CREATE INDEX idx_tickets_user ON tickets(user_id);
|
||||
CREATE INDEX idx_tickets_apartment ON tickets(apartment_id);
|
||||
CREATE INDEX idx_apartments_building ON apartments(building_id);
|
||||
CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id);
|
||||
CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id);
|
||||
|
||||
-- Триггеры для обновления updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Применяем триггеры ко всем таблицам с updated_at
|
||||
DO $$
|
||||
DECLARE
|
||||
t record;
|
||||
BEGIN
|
||||
FOR t IN
|
||||
SELECT table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'updated_at'
|
||||
AND table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE format('CREATE TRIGGER trigger_%s_updated_at
|
||||
BEFORE UPDATE ON %I
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at()',
|
||||
t.table_name, t.table_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -1,53 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все дополнительные сервисы
|
||||
router.get('/additional-services', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { data, error } = await supabase.from('additional_services').select('*').order('created_at', { ascending: false });
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить сервис по id
|
||||
router.get('/additional-services/:id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { id } = req.params;
|
||||
const { data, error } = await supabase.from('additional_services').select('*').eq('id', id).single();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Создать сервис
|
||||
router.post('/additional-services', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { title, description, category, price, image_url } = req.body;
|
||||
const { data, error } = await supabase.from('additional_services').insert([
|
||||
{ title, description, category, price, image_url }
|
||||
]).select().single();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Обновить сервис
|
||||
router.put('/additional-services/:id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { id } = req.params;
|
||||
const { title, description, category, price, image_url } = req.body;
|
||||
const { data, error } = await supabase.from('additional_services').update({
|
||||
title, description, category, price, image_url
|
||||
}).eq('id', id).select().single();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Удалить сервис
|
||||
router.delete('/additional-services/:id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { id } = req.params;
|
||||
const { error } = await supabase.from('additional_services').delete().eq('id', id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,45 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все квартиры по дому
|
||||
router.get('/apartments', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id } = req.query;
|
||||
if (!building_id) return res.status(400).json({ error: 'building_id required' });
|
||||
const { data, error } = await supabase.from('apartments').select('*').eq('building_id', building_id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить адрес квартиры и название дома по id квартиры
|
||||
router.get('/apartment-info', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { apartment_id } = req.query;
|
||||
if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' });
|
||||
|
||||
// Получаем квартиру с building_id и номером
|
||||
const { data: apartment, error: err1 } = await supabase
|
||||
.from('apartments')
|
||||
.select('id, number, building_id')
|
||||
.eq('id', apartment_id)
|
||||
.single();
|
||||
if (err1) return res.status(400).json({ error: err1.message });
|
||||
|
||||
// Получаем дом по building_id
|
||||
const { data: building, error: err2 } = await supabase
|
||||
.from('buildings')
|
||||
.select('id, name, address')
|
||||
.eq('id', apartment.building_id)
|
||||
.single();
|
||||
if (err2) return res.status(400).json({ error: err2.message });
|
||||
|
||||
res.json({
|
||||
apartment_id: apartment.id,
|
||||
apartment_number: apartment.number,
|
||||
building_id: building.id,
|
||||
building_name: building.name,
|
||||
building_address: building.address
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,51 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// POST /sign-in
|
||||
router.post('/sign-in', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
const supabase = getSupabaseClient();
|
||||
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// POST /sign-up
|
||||
router.post('/sign-up', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
const supabase = getSupabaseClient();
|
||||
const { data, error } = await supabase.auth.signUp({ email, password });
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// POST /sign-out
|
||||
router.post('/sign-out', async (req, res) => {
|
||||
const { access_token } = req.body;
|
||||
const supabase = getSupabaseClient();
|
||||
supabase.auth.setSession({ access_token, refresh_token: '' });
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// POST /reset-password
|
||||
router.post('/reset-password', async (req, res) => {
|
||||
const { email } = req.body;
|
||||
const supabase = getSupabaseClient();
|
||||
const { data, error } = await supabase.auth.resetPasswordForEmail(email);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// POST /update-password
|
||||
router.post('/update-password', async (req, res) => {
|
||||
const { access_token, newPassword } = req.body;
|
||||
const supabase = getSupabaseClient();
|
||||
supabase.auth.setSession({ access_token, refresh_token: '' });
|
||||
const { data, error } = await supabase.auth.updateUser({ password: newPassword });
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,14 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все дома по УК
|
||||
router.get('/buildings', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { management_company_id } = req.query;
|
||||
if (!management_company_id) return res.status(400).json({ error: 'management_company_id required' });
|
||||
const { data, error } = await supabase.from('buildings').select('*').eq('management_company_id', management_company_id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,28 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все камеры по дому
|
||||
router.get('/cameras', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id } = req.query;
|
||||
if (!building_id) return res.status(400).json({ error: 'building_id required' });
|
||||
const { data, error } = await supabase.from('cameras').select('*').eq('building_id', building_id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить все камеры по квартире (через building_id)
|
||||
router.get('/cameras/by-apartment', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { apartment_id } = req.query;
|
||||
if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' });
|
||||
// Получаем building_id квартиры и сразу камеры этого дома
|
||||
const { data, error } = await supabase
|
||||
.from('cameras')
|
||||
.select('*, apartments!inner(id, building_id)')
|
||||
.eq('apartments.id', apartment_id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,78 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import gigachat from './gigachat';
|
||||
|
||||
export interface ModerationResult {
|
||||
comment: string;
|
||||
isApproved: boolean;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ChatModerationAgent {
|
||||
private moderationLlm: any;
|
||||
|
||||
constructor(GIGA_AUTH) {
|
||||
// Создаем структурированный вывод для модерации
|
||||
this.moderationLlm = gigachat(GIGA_AUTH).withStructuredOutput(z.object({
|
||||
comment: z.string(),
|
||||
isApproved: z.boolean(),
|
||||
}) as any);
|
||||
}
|
||||
|
||||
private getSystemPrompt(): string {
|
||||
return `Ты модерируешь сообщения в чате. Твоя задача - проверить сообщение на нецензурную лексику, брань и неприемлемый контент.
|
||||
|
||||
Твои задачи:
|
||||
1. Проверь сообщение на наличие нецензурной лексики, мата, ругательств и брани.
|
||||
2. Проверь на оскорбления, угрозы и агрессивное поведение.
|
||||
3. Проверь на спам и рекламу.
|
||||
4. Проверь на неприемлемый контент (дискриминация, экстремизм и т.д.).
|
||||
|
||||
- Если сообщение не содержит запрещенного контента, оно одобряется (isApproved: true).
|
||||
- Если сообщение содержит запрещенный контент, оно отклоняется (isApproved: false).
|
||||
|
||||
Правила написания комментария:
|
||||
- Если сообщение одобряется, оставь поле comment пустым.
|
||||
- Если сообщение отклоняется, пиши комментарий со следующей формулировкой:
|
||||
"Сообщение удалено. Причина: (укажи конкретную причину: нецензурная лексика, оскорбления, спам и т.д.)"`;
|
||||
}
|
||||
|
||||
public async moderateMessage(message: string): Promise<ModerationResult> {
|
||||
try {
|
||||
const prompt = `${this.getSystemPrompt()}
|
||||
|
||||
Сообщение: ${message}`;
|
||||
|
||||
const result = await this.moderationLlm.invoke(prompt);
|
||||
|
||||
// Дополнительная проверка
|
||||
if (!result.isApproved && result.comment.trim() === '') {
|
||||
result.comment = 'Сообщение удалено. Причина: нарушение правил чата.';
|
||||
}
|
||||
|
||||
return {
|
||||
comment: result.comment,
|
||||
isApproved: result.isApproved,
|
||||
success: true
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [Chat Moderation] Ошибка при модерации:', error);
|
||||
|
||||
// В случае ошибки одобряем сообщение
|
||||
return {
|
||||
comment: '',
|
||||
isApproved: true,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем функцию для обратной совместимости
|
||||
export const moderationText = async (title: string, body: string, GIGA_AUTH): Promise<[string, boolean, string]> => {
|
||||
const agent = new ChatModerationAgent(GIGA_AUTH);
|
||||
const result = await agent.moderateMessage(body);
|
||||
return [result.comment, result.isApproved, body];
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Agent } from 'node:https';
|
||||
import { GigaChat } from 'langchain-gigachat';
|
||||
|
||||
const httpsAgent = new Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js)
|
||||
export const gigachat = (GIGA_AUTH) => new
|
||||
GigaChat({
|
||||
model: 'GigaChat-2',
|
||||
scope: 'GIGACHAT_API_PERS',
|
||||
streaming: false,
|
||||
credentials: GIGA_AUTH,
|
||||
httpsAgent
|
||||
});
|
||||
|
||||
export default gigachat;
|
||||
@@ -1,16 +0,0 @@
|
||||
// Конфигурация системы модерации
|
||||
const MODERATION_CONFIG = {
|
||||
// Задержка перед запуском модерации (в миллисекундах)
|
||||
MODERATION_DELAY: 1500, // 1.5 секунды
|
||||
|
||||
// Включена ли система модерации
|
||||
MODERATION_ENABLED: true,
|
||||
|
||||
// Текст для замены заблокированных сообщений
|
||||
BLOCKED_MESSAGE_TEXT: '[Удалено модератором]',
|
||||
|
||||
// Логировать ли процесс модерации
|
||||
ENABLE_MODERATION_LOGS: true
|
||||
};
|
||||
|
||||
module.exports = MODERATION_CONFIG;
|
||||
@@ -1,218 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все чаты по дому
|
||||
router.get('/chats', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id } = req.query;
|
||||
|
||||
if (!building_id) {
|
||||
return res.status(400).json({ error: 'building_id required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
res.json(data || []);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить все чаты по квартире (через building_id)
|
||||
router.get('/chats/by-apartment', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { apartment_id } = req.query;
|
||||
if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' });
|
||||
// Получаем building_id квартиры и сразу чаты этого дома
|
||||
const { data, error } = await supabase
|
||||
.from('chats')
|
||||
.select('*, apartments!inner(id, building_id)')
|
||||
.eq('apartments.id', apartment_id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Создать новый чат
|
||||
router.post('/chats', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id, name } = req.body;
|
||||
|
||||
if (!building_id) {
|
||||
return res.status(400).json({ error: 'building_id is required' });
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chats')
|
||||
.insert({ building_id, name })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить конкретный чат по ID
|
||||
router.get('/chats/:chat_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { chat_id } = req.params;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chats')
|
||||
.select('*')
|
||||
.eq('id', chat_id)
|
||||
.single();
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Обновить чат
|
||||
router.put('/chats/:chat_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { chat_id } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chats')
|
||||
.update({ name })
|
||||
.eq('id', chat_id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Удалить чат
|
||||
router.delete('/chats/:chat_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { chat_id } = req.params;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('chats')
|
||||
.delete()
|
||||
.eq('id', chat_id);
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ success: true, message: 'Chat deleted successfully' });
|
||||
});
|
||||
|
||||
// Получить статистику чата (количество сообщений, участников и т.д.)
|
||||
router.get('/chats/:chat_id/stats', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { chat_id } = req.params;
|
||||
|
||||
try {
|
||||
// Получаем количество сообщений
|
||||
const { count: messageCount, error: messageError } = await supabase
|
||||
.from('messages')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('chat_id', chat_id);
|
||||
|
||||
if (messageError) throw messageError;
|
||||
|
||||
// Получаем информацию о чате с домом
|
||||
const { data: chatInfo, error: chatError } = await supabase
|
||||
.from('chats')
|
||||
.select(`
|
||||
*,
|
||||
buildings (
|
||||
id,
|
||||
name,
|
||||
address,
|
||||
apartments (
|
||||
apartment_residents (
|
||||
user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('id', chat_id)
|
||||
.single();
|
||||
|
||||
if (chatError) throw chatError;
|
||||
|
||||
// Собираем уникальные user_id жителей дома
|
||||
const userIds = new Set();
|
||||
chatInfo.buildings.apartments.forEach(apartment => {
|
||||
apartment.apartment_residents.forEach(resident => {
|
||||
userIds.add(resident.user_id);
|
||||
});
|
||||
});
|
||||
|
||||
// Получаем профили всех жителей
|
||||
let uniqueResidents = [];
|
||||
if (userIds.size > 0) {
|
||||
const { data: profiles } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.in('id', Array.from(userIds));
|
||||
|
||||
uniqueResidents = profiles || [];
|
||||
}
|
||||
|
||||
res.json({
|
||||
chat_id,
|
||||
chat_name: chatInfo.name,
|
||||
building: {
|
||||
id: chatInfo.buildings.id,
|
||||
name: chatInfo.buildings.name,
|
||||
address: chatInfo.buildings.address
|
||||
},
|
||||
message_count: messageCount || 0,
|
||||
total_residents: uniqueResidents.length,
|
||||
residents: uniqueResidents
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить последнее сообщение в чате
|
||||
router.get('/chats/:chat_id/last-message', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { chat_id } = req.params;
|
||||
|
||||
try {
|
||||
// Получаем последнее сообщение
|
||||
const { data: lastMessage, error } = await supabase
|
||||
.from('messages')
|
||||
.select('*')
|
||||
.eq('chat_id', chat_id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
let data = null;
|
||||
|
||||
if (error && error.code === 'PGRST116') {
|
||||
data = null;
|
||||
} else if (error) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
} else if (lastMessage) {
|
||||
// Получаем профиль пользователя для сообщения
|
||||
const { data: userProfile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.eq('id', lastMessage.user_id)
|
||||
.single();
|
||||
|
||||
// Объединяем сообщение с профилем
|
||||
data = {
|
||||
...lastMessage,
|
||||
user_profiles: userProfile || null
|
||||
};
|
||||
}
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,90 +0,0 @@
|
||||
const getSupabaseUrl = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].SUPABASE_URL.value;
|
||||
};
|
||||
|
||||
const getSupabaseKey = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].SUPABASE_KEY.value;
|
||||
};
|
||||
|
||||
const getSupabaseServiceKey = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].SUPABASE_SERVICE_KEY.value;
|
||||
};
|
||||
|
||||
const getGigaAuth = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].GIGA_AUTH.value;
|
||||
};
|
||||
|
||||
const getLangsmithApiKey = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].LANGSMITH_API_KEY.value;
|
||||
};
|
||||
|
||||
const getLangsmithEndpoint = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].LANGSMITH_ENDPOINT.value;
|
||||
};
|
||||
|
||||
const getLangsmithTracing = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].LANGSMITH_TRACING.value;
|
||||
};
|
||||
|
||||
const getLangsmithProject = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].LANGSMITH_PROJECT.value;
|
||||
};
|
||||
|
||||
const getTavilyApiKey = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].TAVILY_API_KEY.value;
|
||||
};
|
||||
|
||||
const getRagSupabaseServiceRoleKey = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].RAG_SUPABASE_SERVICE_ROLE_KEY.value;
|
||||
};
|
||||
|
||||
const getRagSupabaseUrl = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||
const data = await response.json();
|
||||
return data.features['sber_mobile'].RAG_SUPABASE_URL.value;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getSupabaseUrl,
|
||||
getSupabaseKey,
|
||||
getSupabaseServiceKey,
|
||||
getGigaAuth
|
||||
};
|
||||
|
||||
// IIFE для установки переменных окружения
|
||||
(async () => {
|
||||
try {
|
||||
process.env.GIGA_AUTH = await getGigaAuth();
|
||||
process.env.LANGSMITH_API_KEY = await getLangsmithApiKey();
|
||||
process.env.LANGSMITH_ENDPOINT = await getLangsmithEndpoint();
|
||||
process.env.LANGSMITH_TRACING = await getLangsmithTracing();
|
||||
process.env.LANGSMITH_PROJECT = await getLangsmithProject();
|
||||
process.env.TAVILY_API_KEY = await getTavilyApiKey();
|
||||
process.env.RAG_SUPABASE_SERVICE_ROLE_KEY = await getRagSupabaseServiceRoleKey();
|
||||
process.env.RAG_SUPABASE_URL = await getRagSupabaseUrl();
|
||||
|
||||
console.log('Environment variables loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error loading environment variables:', error);
|
||||
}
|
||||
})();
|
||||
@@ -1,41 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const authRouter = require('./auth');
|
||||
const { supabaseRouter } = require('./supabaseClient');
|
||||
const profileRouter = require('./profile');
|
||||
const initiativesRouter = require('./initiatives');
|
||||
const votesRouter = require('./votes');
|
||||
const additionalServicesRouter = require('./additional_services');
|
||||
const chatsRouter = require('./chats');
|
||||
const camerasRouter = require('./cameras');
|
||||
const ticketsRouter = require('./tickets');
|
||||
const messagesRouter = require('./messages');
|
||||
const moderationRouter = require('./moderation');
|
||||
const utilityPaymentsRouter = require('./utility_payments');
|
||||
const apartmentsRouter = require('./apartments');
|
||||
const buildingsRouter = require('./buildings');
|
||||
const userApartmentsRouter = require('./user_apartments');
|
||||
const avatarRouter = require('./media');
|
||||
const supportRouter = require('./supportApi');
|
||||
const moderateRouter = require('./moderate.js');
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
router.use('/auth', authRouter);
|
||||
router.use('/supabase', supabaseRouter);
|
||||
router.use('', profileRouter);
|
||||
router.use('', initiativesRouter);
|
||||
router.use('', votesRouter);
|
||||
router.use('', additionalServicesRouter);
|
||||
router.use('', chatsRouter);
|
||||
router.use('', camerasRouter);
|
||||
router.use('', ticketsRouter);
|
||||
router.use('', messagesRouter);
|
||||
router.use('', moderationRouter);
|
||||
router.use('', utilityPaymentsRouter);
|
||||
router.use('', apartmentsRouter);
|
||||
router.use('', buildingsRouter);
|
||||
router.use('', userApartmentsRouter);
|
||||
router.use('', avatarRouter);
|
||||
router.use('', supportRouter);
|
||||
router.use('', moderateRouter);
|
||||
@@ -1,22 +0,0 @@
|
||||
import { GigaChat as GigaChatLang} from 'langchain-gigachat';
|
||||
import { GigaChat } from 'gigachat';
|
||||
import { Agent } from 'node:https';
|
||||
|
||||
const httpsAgent = new Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
export const llm_mod = (GIGA_AUTH) =>
|
||||
new GigaChatLang({
|
||||
credentials: GIGA_AUTH,
|
||||
temperature: 0.2,
|
||||
model: 'GigaChat-2-Max',
|
||||
httpsAgent,
|
||||
});
|
||||
|
||||
export const llm_gen = (GIGA_AUTH) =>
|
||||
new GigaChat({
|
||||
credentials: GIGA_AUTH,
|
||||
model: 'GigaChat-2',
|
||||
httpsAgent,
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { llm_mod } from './llm'
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
// возвращаю комментарий + исправленное предложение + булево значение
|
||||
|
||||
export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => {
|
||||
|
||||
const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({
|
||||
comment: z.string(),
|
||||
fixedText: z.string().optional(),
|
||||
isApproved: z.boolean(),
|
||||
}) as any)
|
||||
|
||||
const prompt = `
|
||||
Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения,
|
||||
не имеющая отношения к Управляющей компании).
|
||||
|
||||
Заголовок: ${title}
|
||||
Основной текст: ${description}
|
||||
|
||||
Твои задачи:
|
||||
1. Проверь предложение и заголовок на спам.
|
||||
2. Проверь, чтобы заголовок и текст были на одну тему.
|
||||
3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей.
|
||||
4. Проверь грамматику.
|
||||
5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы.
|
||||
6. Не должно быть рекламы, ссылок и т.д.
|
||||
7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы.
|
||||
8. Предложение должно быть в вежливой форме.
|
||||
|
||||
- Если все правила соблюдены, то предложение принимается!
|
||||
|
||||
- Если предложение отклонено, всегда пиши комментарий и fixedText!
|
||||
|
||||
Правила написания комментария:
|
||||
- Если предложение отклоняется, пиши комментарий со следующей формулировкой:
|
||||
"Предложение отклонено. Причина: (укажи проблему)"
|
||||
|
||||
Правила написания fixedText:
|
||||
- Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам.
|
||||
- Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию,
|
||||
которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл.
|
||||
- Если текст не представляет никакой ценности, возврати в поле "fixedText" правило,
|
||||
по которому оно не прошло.
|
||||
-Если предложение принимается, то ничего не возвращай в поле fixedText.
|
||||
`
|
||||
|
||||
const result = await moderationLlm.invoke(prompt);
|
||||
if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) {
|
||||
result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.',
|
||||
result.fixedText = description
|
||||
}
|
||||
|
||||
return [result.comment, result.fixedText, result.isApproved];
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { llm_gen } from './llm'
|
||||
import { detectImage } from 'gigachat';
|
||||
|
||||
export const generatePicture = async (prompt: string, GIGA_AUTH) => {
|
||||
const resp = await llm_gen(GIGA_AUTH).chat({
|
||||
messages: [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Ты — Василий Кандинский для жильцов многоквартирного дома"
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Старайся передать атмосферу уюта и безопасности.
|
||||
Нарисуй картинку подходящую для такого события: ${prompt}
|
||||
В картинке не должно быть текста, только изображение.`,
|
||||
},
|
||||
],
|
||||
function_call: 'auto',
|
||||
});
|
||||
|
||||
// Получение изображения по идентификатору
|
||||
const detectedImage = detectImage(resp.choices[0]?.message.content ?? '');
|
||||
|
||||
if (!detectedImage?.uuid) {
|
||||
throw new Error('Не удалось получить UUID изображения из ответа GigaChat');
|
||||
}
|
||||
|
||||
const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid);
|
||||
|
||||
// Возвращаем содержимое изображения, убеждаясь что это Buffer
|
||||
if (Buffer.isBuffer(image.content)) {
|
||||
return image.content;
|
||||
} else if (typeof image.content === 'string') {
|
||||
return Buffer.from(image.content, 'binary');
|
||||
} else {
|
||||
throw new Error('Unexpected image content type: ' + typeof image.content);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все предложения, инициативы status=review (по дому)
|
||||
router.get('/initiatives-review', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id } = req.query;
|
||||
let query = supabase.from('initiatives').select('*').eq('status', 'review');
|
||||
if (building_id) query = query.eq('building_id', building_id);
|
||||
const { data, error } = await query;
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить все сборы, инициативы status=fundraising (по дому)
|
||||
router.get('/initiatives-fundraising', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id } = req.query;
|
||||
let query = supabase.from('initiatives').select('*').eq('status', 'fundraising');
|
||||
if (building_id) query = query.eq('building_id', building_id);
|
||||
const { data, error } = await query;
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить инициативу по id (и optionally building_id)
|
||||
router.get('/initiatives/:id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { id } = req.params;
|
||||
const { building_id } = req.query;
|
||||
let query = supabase.from('initiatives').select('*').eq('id', id);
|
||||
if (building_id) query = query.eq('building_id', building_id);
|
||||
const { data, error } = await query.single();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Создать инициативу
|
||||
router.post('/initiatives', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id, creator_id, title, description, status, target_amount, current_amount, image_url } = req.body;
|
||||
const { data, error } = await supabase.from('initiatives').insert([
|
||||
{ building_id, creator_id, title, description, status, target_amount, current_amount: current_amount || 0, image_url }
|
||||
]).select().single();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Обновить инициативу
|
||||
router.put('/initiatives/:id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { id } = req.params;
|
||||
const { title, description, status, target_amount, current_amount, image_url } = req.body;
|
||||
const { data, error } = await supabase.from('initiatives').update({
|
||||
title, description, status, target_amount, current_amount, image_url
|
||||
}).eq('id', id).select().single();
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Удалить инициативу
|
||||
router.delete('/initiatives/:id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { id } = req.params;
|
||||
const { error } = await supabase.from('initiatives').delete().eq('id', id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Получить все инициативы по квартире с голосами пользователя
|
||||
router.get('/initiatives/by-apartment', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { apartment_id, user_id } = req.query;
|
||||
if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' });
|
||||
// Получаем building_id квартиры
|
||||
const { data: apartments, error: err1 } = await supabase
|
||||
.from('apartments')
|
||||
.select('building_id')
|
||||
.eq('id', apartment_id)
|
||||
.single();
|
||||
if (err1) return res.status(400).json({ error: err1.message });
|
||||
const building_id = apartments.building_id;
|
||||
// Получаем инициативы этого дома с голосами пользователя (если user_id передан)
|
||||
let selectStr = '*, votes:initiatives(id, votes!left(user_id, vote_type))';
|
||||
if (!user_id) selectStr = '*';
|
||||
const { data, error } = await supabase
|
||||
.from('initiatives')
|
||||
.select(selectStr)
|
||||
.eq('building_id', building_id);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
// Если user_id передан, фильтруем только голос текущего пользователя
|
||||
if (user_id && data) {
|
||||
data.forEach(initiative => {
|
||||
initiative.user_vote = (initiative.votes || []).find(v => v.user_id === user_id) || null;
|
||||
delete initiative.votes;
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,15 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { supabaseRouter } = require('./supabaseClient');
|
||||
|
||||
|
||||
// GET /avatar
|
||||
router.get('/avatar', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { user_id } = req.query;
|
||||
if (!user_id) return res.status(400).json({ error: 'user_id required' });
|
||||
const { data, error } = await supabase.storage.from('avatars').download(`avatar_${user_id}.png`);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.blob(data);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,235 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации
|
||||
const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации
|
||||
|
||||
|
||||
// Добавляем middleware для логирования всех запросов к messages роутеру
|
||||
|
||||
// Тестовый эндпоинт для проверки работы роутера
|
||||
router.get('/messages/test', (req, res) => {
|
||||
res.json({
|
||||
status: 'OK',
|
||||
message: 'Messages router работает',
|
||||
timestamp: new Date().toISOString(),
|
||||
moderation_config: MODERATION_CONFIG
|
||||
});
|
||||
});
|
||||
|
||||
// Получить все сообщения в чате с информацией о пользователе
|
||||
router.get('/messages', async (req, res) => {
|
||||
try {
|
||||
const { chat_id, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
if (!chat_id) {
|
||||
return res.status(400).json({ error: 'chat_id is required' });
|
||||
}
|
||||
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('messages')
|
||||
.select(`
|
||||
*,
|
||||
user_profiles (
|
||||
id,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.eq('chat_id', chat_id)
|
||||
.order('created_at', { ascending: true })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (error) {
|
||||
return res.status(500).json({ error: 'Failed to fetch messages' });
|
||||
}
|
||||
|
||||
// Получаем уникальные ID пользователей из сообщений, у которых нет профиля
|
||||
const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles);
|
||||
const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))];
|
||||
|
||||
if (userIds.length > 0) {
|
||||
const { data: profiles, error: profilesError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.in('id', userIds);
|
||||
|
||||
if (!profilesError && profiles) {
|
||||
// Добавляем профили к сообщениям
|
||||
data.forEach(message => {
|
||||
if (!message.user_profiles) {
|
||||
message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Unexpected error occurred' });
|
||||
}
|
||||
});
|
||||
|
||||
// Создать новое сообщение
|
||||
router.post('/messages', async (req, res) => {
|
||||
|
||||
let supabase;
|
||||
try {
|
||||
supabase = getSupabaseClient();
|
||||
} catch (error) {
|
||||
console.error(`❌ [Message Send] Ошибка получения Supabase клиента:`, error);
|
||||
return res.status(500).json({ error: 'Database connection error' });
|
||||
}
|
||||
|
||||
const { chat_id, user_id, text } = req.body;
|
||||
|
||||
|
||||
if (!chat_id || !user_id || !text) {
|
||||
console.log(`❌ [Message Send] Отклонен: отсутствуют обязательные поля`);
|
||||
console.log(`❌ [Message Send] chat_id: ${chat_id}, user_id: ${user_id}, text: ${text}`);
|
||||
return res.status(400).json({
|
||||
error: 'chat_id, user_id, and text are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Создаем сообщение
|
||||
const { data: newMessage, error } = await supabase
|
||||
.from('messages')
|
||||
.insert({ chat_id, user_id, text })
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error(`❌ [Message Send] Ошибка сохранения в Supabase:`, error);
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
// Получаем профиль пользователя
|
||||
const { data: userProfile, error: profileError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.eq('id', user_id)
|
||||
.single();
|
||||
|
||||
if (profileError) {
|
||||
console.log(`⚠️ [Message Send] Профиль пользователя не найден:`, profileError);
|
||||
}
|
||||
|
||||
// Объединяем сообщение с профилем
|
||||
const data = {
|
||||
...newMessage,
|
||||
user_profiles: userProfile || null
|
||||
};
|
||||
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить конкретное сообщение
|
||||
router.get('/messages/:message_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { message_id } = req.params;
|
||||
|
||||
// Получаем сообщение
|
||||
const { data: message, error } = await supabase
|
||||
.from('messages')
|
||||
.select('*')
|
||||
.eq('id', message_id)
|
||||
.single();
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
|
||||
// Получаем профиль пользователя
|
||||
const { data: userProfile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.eq('id', message.user_id)
|
||||
.single();
|
||||
|
||||
// Объединяем сообщение с профилем
|
||||
const data = {
|
||||
...message,
|
||||
user_profiles: userProfile || null
|
||||
};
|
||||
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить последние сообщения для каждого чата (для списка чатов)
|
||||
router.get('/chats/last-messages', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { building_id } = req.query;
|
||||
|
||||
if (!building_id) {
|
||||
return res.status(400).json({ error: 'building_id required' });
|
||||
}
|
||||
|
||||
// Получаем чаты и их последние сообщения через обычные запросы
|
||||
const { data: chats, error: chatsError } = await supabase
|
||||
.from('chats')
|
||||
.select('*')
|
||||
.eq('building_id', building_id);
|
||||
|
||||
if (chatsError) return res.status(400).json({ error: chatsError.message });
|
||||
|
||||
// Для каждого чата получаем последнее сообщение
|
||||
const chatsWithMessages = await Promise.all(
|
||||
chats.map(async (chat) => {
|
||||
const { data: lastMessage } = await supabase
|
||||
.from('messages')
|
||||
.select(`
|
||||
*,
|
||||
user_profiles:user_id (
|
||||
id,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.eq('chat_id', chat.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
return {
|
||||
...chat,
|
||||
last_message: lastMessage || null
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json(chatsWithMessages);
|
||||
});
|
||||
|
||||
// Удалить сообщение (только для автора)
|
||||
router.delete('/messages/:message_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { message_id } = req.params;
|
||||
const { user_id } = req.body;
|
||||
|
||||
if (!user_id) {
|
||||
return res.status(400).json({ error: 'user_id required' });
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь является автором сообщения
|
||||
const { data: message, error: fetchError } = await supabase
|
||||
.from('messages')
|
||||
.select('user_id')
|
||||
.eq('id', message_id)
|
||||
.single();
|
||||
|
||||
if (fetchError) return res.status(400).json({ error: fetchError.message });
|
||||
|
||||
if (message.user_id !== user_id) {
|
||||
return res.status(403).json({ error: 'You can only delete your own messages' });
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('messages')
|
||||
.delete()
|
||||
.eq('id', message_id);
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ success: true, message: 'Message deleted successfully' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,162 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { moderationText } = require('./initiatives-ai-agents/moderation');
|
||||
const { generatePicture } = require('./initiatives-ai-agents/picture');
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
const { getGigaAuth } = require('./get-constants');
|
||||
|
||||
async function getGigaKey() {
|
||||
const GIGA_AUTH = await getGigaAuth();
|
||||
return GIGA_AUTH;
|
||||
}
|
||||
|
||||
// Обработчик для модерации и создания инициативы
|
||||
router.post('/moderate', async (req, res) => {
|
||||
|
||||
const GIGA_AUTH = await getGigaKey();
|
||||
|
||||
try {
|
||||
const { title, description, building_id, creator_id, target_amount, status } = req.body;
|
||||
|
||||
if (!title || !description) {
|
||||
res.status(400).json({ error: 'Заголовок и описание обязательны' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!building_id || !creator_id) {
|
||||
res.status(400).json({ error: 'ID дома и создателя обязательны' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация статуса, если передан
|
||||
const validStatuses = ['moderation', 'review', 'fundraising', 'approved', 'rejected'];
|
||||
if (status && !validStatuses.includes(status)) {
|
||||
res.status(400).json({ error: `Недопустимый статус. Допустимые значения: ${validStatuses.join(', ')}` });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Запрос на модерацию:', { title: title.substring(0, 50), description: description.substring(0, 100) });
|
||||
|
||||
// Модерация текста (передаем title и description как body)
|
||||
const [comment, fixedText, isApproved] = await moderationText(title, description, GIGA_AUTH);
|
||||
|
||||
// Если модерация не прошла, возвращаем undefined
|
||||
if (!isApproved) {
|
||||
if (!comment || comment.trim() === '') {
|
||||
console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении');
|
||||
}
|
||||
|
||||
res.json({
|
||||
comment,
|
||||
fixedText,
|
||||
isApproved,
|
||||
initiative: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Модерация прошла, генерируем изображение используя заголовок как промпт
|
||||
console.log('Модерация прошла, генерируем изображение с промптом:', title);
|
||||
|
||||
const imageBuffer = await generatePicture(title, GIGA_AUTH);
|
||||
|
||||
if (!imageBuffer || imageBuffer.length === 0) {
|
||||
res.status(500).json({ error: 'Получен пустой буфер изображения' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем Supabase клиент и создаем имя файла
|
||||
const supabase = getSupabaseClient();
|
||||
const timestamp = Date.now();
|
||||
const filename = `image_${creator_id}_${timestamp}.jpg`;
|
||||
|
||||
// Загружаем изображение в Supabase Storage
|
||||
let uploadResult;
|
||||
let retries = 0;
|
||||
const maxRetries = 5;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
uploadResult = await supabase.storage
|
||||
.from('images')
|
||||
.upload(filename, imageBuffer, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (!uploadResult.error) {
|
||||
break; // Успешная загрузка
|
||||
}
|
||||
|
||||
retries++;
|
||||
|
||||
if (retries < maxRetries) {
|
||||
// Ждем перед повторной попыткой
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * retries));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message);
|
||||
retries++;
|
||||
|
||||
if (retries < maxRetries) {
|
||||
// Ждем перед повторной попыткой
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * retries));
|
||||
} else {
|
||||
throw error; // Перебрасываем ошибку после всех попыток
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadResult?.error) {
|
||||
console.error('Supabase storage error after all retries:', uploadResult.error);
|
||||
res.status(500).json({ error: 'Ошибка при сохранении изображения после нескольких попыток' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Изображение успешно загружено в Supabase Storage:', filename);
|
||||
|
||||
// Получаем публичный URL
|
||||
const { data: urlData } = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(filename);
|
||||
|
||||
// Определяем статус: если передан в запросе, используем его, иначе 'review'
|
||||
const finalStatus = status || 'review';
|
||||
|
||||
// Создаем инициативу в базе данных
|
||||
const { data: initiative, error: initiativeError } = await supabase
|
||||
.from('initiatives')
|
||||
.insert([{
|
||||
building_id,
|
||||
creator_id,
|
||||
title: fixedText || title,
|
||||
description,
|
||||
status: finalStatus,
|
||||
target_amount: target_amount || null,
|
||||
current_amount: 0,
|
||||
image_url: urlData.publicUrl
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (initiativeError) {
|
||||
console.error('Ошибка создания инициативы:', initiativeError);
|
||||
res.status(500).json({ error: 'Ошибка при создании инициативы', details: initiativeError.message });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Инициатива успешно создана:', initiative.id);
|
||||
|
||||
res.json({
|
||||
comment,
|
||||
fixedText,
|
||||
isApproved,
|
||||
initiative
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in moderation and initiative creation:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,53 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config');
|
||||
const { moderationText } = require('./chat-ai-agent/chat-moderation');
|
||||
|
||||
// Получить текущие настройки модерации
|
||||
router.get('/moderation/config', (req, res) => {
|
||||
res.json(MODERATION_CONFIG);
|
||||
});
|
||||
|
||||
// Обновить настройки модерации
|
||||
router.post('/moderation/config', (req, res) => {
|
||||
|
||||
const oldConfig = { ...MODERATION_CONFIG };
|
||||
const { MODERATION_DELAY, MODERATION_ENABLED, BLOCKED_MESSAGE_TEXT, ENABLE_MODERATION_LOGS } = req.body;
|
||||
|
||||
const changes = [];
|
||||
|
||||
if (MODERATION_DELAY !== undefined) {
|
||||
const newValue = parseInt(MODERATION_DELAY);
|
||||
MODERATION_CONFIG.MODERATION_DELAY = newValue;
|
||||
changes.push(`MODERATION_DELAY: ${oldConfig.MODERATION_DELAY} -> ${newValue}`);
|
||||
}
|
||||
if (MODERATION_ENABLED !== undefined) {
|
||||
const newValue = Boolean(MODERATION_ENABLED);
|
||||
MODERATION_CONFIG.MODERATION_ENABLED = newValue;
|
||||
changes.push(`MODERATION_ENABLED: ${oldConfig.MODERATION_ENABLED} -> ${newValue}`);
|
||||
}
|
||||
if (BLOCKED_MESSAGE_TEXT !== undefined) {
|
||||
const newValue = String(BLOCKED_MESSAGE_TEXT);
|
||||
MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT = newValue;
|
||||
changes.push(`BLOCKED_MESSAGE_TEXT: "${oldConfig.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`);
|
||||
}
|
||||
if (ENABLE_MODERATION_LOGS !== undefined) {
|
||||
const newValue = Boolean(ENABLE_MODERATION_LOGS)
|
||||
MODERATION_CONFIG.ENABLE_MODERATION_LOGS = newValue;
|
||||
changes.push(`ENABLE_MODERATION_LOGS: ${oldConfig.ENABLE_MODERATION_LOGS} -> ${newValue}`);
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
changes.forEach((change, index) => {
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Настройки модерации обновлены',
|
||||
changes: changes,
|
||||
config: MODERATION_CONFIG
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,982 +0,0 @@
|
||||
const { getSupabaseClient, initializationPromise } = require('./supabaseClient');
|
||||
const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config');
|
||||
const { getGigaAuth } = require('./get-constants');
|
||||
const { moderationText } = require('./chat-ai-agent/chat-moderation');
|
||||
|
||||
async function getGigaKey() {
|
||||
const GIGA_AUTH = await getGigaAuth();
|
||||
return GIGA_AUTH;
|
||||
}
|
||||
|
||||
class ChatPollingHandler {
|
||||
constructor() {
|
||||
this.connectedClients = new Map(); // user_id -> { user_info, chats: Set(), lastActivity: Date }
|
||||
this.chatParticipants = new Map(); // chat_id -> Set(user_id)
|
||||
this.userEventQueues = new Map(); // user_id -> [{id, event, data, timestamp}]
|
||||
this.eventIdCounter = 0;
|
||||
this.realtimeSubscription = null;
|
||||
|
||||
// Инициализируем Supabase подписку с задержкой и проверками
|
||||
this.initializeWithRetry();
|
||||
|
||||
// Очистка старых событий каждые 5 минут
|
||||
setInterval(() => {
|
||||
this.cleanupOldEvents();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
// Инициализация с повторными попытками
|
||||
async initializeWithRetry() {
|
||||
try {
|
||||
// Сначала ждем завершения основной инициализации
|
||||
await initializationPromise;
|
||||
|
||||
this.setupRealtimeSubscription();
|
||||
this.testRealtimeConnection();
|
||||
return;
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ [Supabase] Основная инициализация неудачна, пробуем альтернативный подход');
|
||||
}
|
||||
|
||||
// Если основная инициализация не удалась, используем повторные попытки
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
const baseDelay = 2000; // 2 секунды
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
attempts++;
|
||||
|
||||
// Ждем перед попыткой
|
||||
await new Promise(resolve => setTimeout(resolve, baseDelay * attempts));
|
||||
|
||||
// Проверяем готовность Supabase клиента
|
||||
const supabase = getSupabaseClient();
|
||||
if (supabase) {
|
||||
this.setupRealtimeSubscription();
|
||||
this.testRealtimeConnection();
|
||||
return; // Успех, выходим
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ [Supabase] Попытка #${attempts} неудачна:`, error.message);
|
||||
|
||||
if (attempts === maxAttempts) {
|
||||
console.error('❌ [Supabase] Все попытки инициализации исчерпаны');
|
||||
console.error('❌ [Supabase] Realtime подписка будет недоступна');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Аутентификация пользователя
|
||||
async handleAuthentication(req, res) {
|
||||
const { user_id, token } = req.body;
|
||||
|
||||
if (!user_id) {
|
||||
res.status(400).json({ error: 'user_id is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем пользователя в базе данных
|
||||
const supabase = getSupabaseClient();
|
||||
const { data: userProfile, error } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*')
|
||||
.eq('id', user_id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.log('❌ [Polling Server] Пользователь не найден:', error);
|
||||
res.status(401).json({ error: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Регистрируем пользователя
|
||||
this.connectedClients.set(user_id, {
|
||||
user_info: {
|
||||
user_id,
|
||||
profile: userProfile,
|
||||
last_seen: new Date()
|
||||
},
|
||||
chats: new Set(),
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
// Создаем очередь событий для пользователя
|
||||
if (!this.userEventQueues.has(user_id)) {
|
||||
this.userEventQueues.set(user_id, []);
|
||||
}
|
||||
|
||||
// Добавляем событие аутентификации в очередь
|
||||
this.addEventToQueue(user_id, 'authenticated', {
|
||||
message: 'Successfully authenticated',
|
||||
user: userProfile
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Successfully authenticated',
|
||||
user: userProfile
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [Polling Server] Ошибка аутентификации:', error);
|
||||
res.status(500).json({ error: 'Authentication failed' });
|
||||
}
|
||||
}
|
||||
|
||||
// Эндпоинт для получения событий (polling)
|
||||
async handleGetEvents(req, res) {
|
||||
try {
|
||||
const { user_id, last_event_id } = req.query;
|
||||
|
||||
if (!user_id) {
|
||||
res.status(400).json({ error: 'user_id is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем время последней активности
|
||||
client.lastActivity = new Date();
|
||||
|
||||
// Получаем очередь событий пользователя
|
||||
const eventQueue = this.userEventQueues.get(user_id) || [];
|
||||
|
||||
// Фильтруем события после last_event_id
|
||||
const lastEventId = parseInt(last_event_id) || 0;
|
||||
const newEvents = eventQueue.filter(event => event.id > lastEventId);
|
||||
|
||||
// Логируем отправку событий клиенту
|
||||
if (newEvents.length > 0) {
|
||||
console.log(`📨 [Polling Server] Отправляем ${newEvents.length} событий клиенту ${user_id}`);
|
||||
newEvents.forEach(event => {
|
||||
if (event.event === 'message_updated') {
|
||||
console.log(`📨 [Polling Server] → Событие: ${event.event}, Сообщение ID: ${event.data?.message?.id}, Текст: "${event.data?.message?.text?.substring(0, 50)}${(event.data?.message?.text?.length || 0) > 50 ? '...' : ''}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
events: newEvents,
|
||||
last_event_id: eventQueue.length > 0 ? Math.max(...eventQueue.map(e => e.id)) : lastEventId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [Polling Server] Ошибка получения событий:', error);
|
||||
res.status(500).json({ error: 'Failed to get events' });
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP эндпоинт для присоединения к чату
|
||||
async handleJoinChat(req, res) {
|
||||
try {
|
||||
const { user_id, chat_id } = req.body;
|
||||
|
||||
if (!user_id || !chat_id) {
|
||||
res.status(400).json({ error: 'user_id and chat_id are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что чат существует и пользователь имеет доступ к нему
|
||||
const supabase = getSupabaseClient();
|
||||
const { data: chat, error } = await supabase
|
||||
.from('chats')
|
||||
.select(`
|
||||
*,
|
||||
buildings (
|
||||
management_company_id,
|
||||
apartments (
|
||||
apartment_residents (
|
||||
user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('id', chat_id)
|
||||
.single();
|
||||
|
||||
if (error || !chat) {
|
||||
res.status(404).json({ error: 'Chat not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем доступ пользователя к чату через квартиры в доме
|
||||
const hasAccess = chat.buildings.apartments.some(apartment =>
|
||||
apartment.apartment_residents.some(resident =>
|
||||
resident.user_id === user_id
|
||||
)
|
||||
);
|
||||
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({ error: 'Access denied to this chat' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем пользователя в чат
|
||||
client.chats.add(chat_id);
|
||||
|
||||
if (!this.chatParticipants.has(chat_id)) {
|
||||
this.chatParticipants.set(chat_id, new Set());
|
||||
}
|
||||
this.chatParticipants.get(chat_id).add(user_id);
|
||||
|
||||
// Добавляем событие присоединения в очередь пользователя
|
||||
this.addEventToQueue(user_id, 'joined_chat', {
|
||||
chat_id,
|
||||
chat: chat,
|
||||
message: 'Successfully joined chat'
|
||||
});
|
||||
|
||||
// Уведомляем других участников о подключении
|
||||
this.broadcastToChatExcludeUser(chat_id, user_id, 'user_joined', {
|
||||
chat_id,
|
||||
user: client.user_info.profile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Joined chat successfully' });
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to join chat' });
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP эндпоинт для покидания чата
|
||||
async handleLeaveChat(req, res) {
|
||||
try {
|
||||
const { user_id, chat_id } = req.body;
|
||||
|
||||
if (!user_id || !chat_id) {
|
||||
res.status(400).json({ error: 'user_id and chat_id are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Удаляем пользователя из чата
|
||||
client.chats.delete(chat_id);
|
||||
|
||||
if (this.chatParticipants.has(chat_id)) {
|
||||
this.chatParticipants.get(chat_id).delete(user_id);
|
||||
|
||||
// Если чат пуст, удаляем его
|
||||
if (this.chatParticipants.get(chat_id).size === 0) {
|
||||
this.chatParticipants.delete(chat_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Уведомляем других участников об отключении
|
||||
this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', {
|
||||
chat_id,
|
||||
user: client.user_info.profile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Left chat successfully' });
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to leave chat' });
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP эндпоинт для отправки сообщения
|
||||
async handleSendMessage(req, res) {
|
||||
try {
|
||||
const { user_id, chat_id, text } = req.body;
|
||||
|
||||
if (!user_id || !chat_id || !text) {
|
||||
res.status(400).json({ error: 'user_id, chat_id and text are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.chats.has(chat_id)) {
|
||||
res.status(403).json({ error: 'Not joined to this chat' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем сообщение в базу данных
|
||||
const supabase = getSupabaseClient();
|
||||
const { data: message, error } = await supabase
|
||||
.from('messages')
|
||||
.insert({
|
||||
chat_id,
|
||||
user_id,
|
||||
text
|
||||
})
|
||||
.select(`
|
||||
*,
|
||||
user_profiles (
|
||||
id,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
res.status(500).json({ error: 'Failed to save message' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Отправляем сообщение всем участникам чата
|
||||
this.broadcastToChat(chat_id, 'new_message', {
|
||||
message,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Message sent successfully' });
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to send message' });
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP эндпоинт для индикации печатания
|
||||
async handleTypingStart(req, res) {
|
||||
try {
|
||||
const { user_id, chat_id } = req.body;
|
||||
|
||||
if (!user_id || !chat_id) {
|
||||
res.status(400).json({ error: 'user_id and chat_id are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.chats.has(chat_id)) {
|
||||
res.status(403).json({ error: 'Not joined to this chat' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_start', {
|
||||
chat_id,
|
||||
user: client.user_info.profile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to send typing indicator' });
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP эндпоинт для остановки индикации печатания
|
||||
async handleTypingStop(req, res) {
|
||||
try {
|
||||
const { user_id, chat_id } = req.body;
|
||||
|
||||
if (!user_id || !chat_id) {
|
||||
res.status(400).json({ error: 'user_id and chat_id are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.chats.has(chat_id)) {
|
||||
res.status(403).json({ error: 'Not joined to this chat' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_stop', {
|
||||
chat_id,
|
||||
user: client.user_info.profile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to send typing indicator' });
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка отключения клиента
|
||||
handleClientDisconnect(user_id) {
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (!client) return;
|
||||
|
||||
// Удаляем пользователя из всех чатов
|
||||
client.chats.forEach(chat_id => {
|
||||
if (this.chatParticipants.has(chat_id)) {
|
||||
this.chatParticipants.get(chat_id).delete(user_id);
|
||||
|
||||
// Уведомляем других участников об отключении
|
||||
this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', {
|
||||
chat_id,
|
||||
user: client.user_info.profile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Если чат пуст, удаляем его
|
||||
if (this.chatParticipants.get(chat_id).size === 0) {
|
||||
this.chatParticipants.delete(chat_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем клиента
|
||||
this.connectedClients.delete(user_id);
|
||||
}
|
||||
|
||||
// Добавление события в очередь пользователя
|
||||
addEventToQueue(user_id, event, data) {
|
||||
if (!this.userEventQueues.has(user_id)) {
|
||||
this.userEventQueues.set(user_id, []);
|
||||
}
|
||||
|
||||
const eventQueue = this.userEventQueues.get(user_id);
|
||||
const eventId = ++this.eventIdCounter;
|
||||
|
||||
eventQueue.push({
|
||||
id: eventId,
|
||||
event,
|
||||
data,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Ограничиваем размер очереди (последние 100 событий)
|
||||
if (eventQueue.length > 100) {
|
||||
eventQueue.splice(0, eventQueue.length - 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Рассылка события всем участникам чата
|
||||
broadcastToChat(chat_id, event, data) {
|
||||
const participants = this.chatParticipants.get(chat_id);
|
||||
if (!participants) return;
|
||||
|
||||
participants.forEach(user_id => {
|
||||
this.addEventToQueue(user_id, event, data);
|
||||
});
|
||||
}
|
||||
|
||||
// Рассылка события всем участникам чата кроме отправителя
|
||||
broadcastToChatExcludeUser(chat_id, exclude_user_id, event, data) {
|
||||
const participants = this.chatParticipants.get(chat_id);
|
||||
if (!participants) return;
|
||||
|
||||
participants.forEach(user_id => {
|
||||
if (user_id !== exclude_user_id) {
|
||||
this.addEventToQueue(user_id, event, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Получение списка онлайн пользователей в чате
|
||||
getOnlineUsersInChat(chat_id) {
|
||||
const participants = this.chatParticipants.get(chat_id) || new Set();
|
||||
const onlineUsers = [];
|
||||
const now = new Date();
|
||||
const ONLINE_THRESHOLD = 2 * 60 * 1000; // 2 минуты
|
||||
|
||||
participants.forEach(user_id => {
|
||||
const client = this.connectedClients.get(user_id);
|
||||
if (client && (now - client.lastActivity) < ONLINE_THRESHOLD) {
|
||||
onlineUsers.push(client.user_info.profile);
|
||||
}
|
||||
});
|
||||
|
||||
return onlineUsers;
|
||||
}
|
||||
|
||||
// Отправка системного сообщения в чат
|
||||
async sendSystemMessage(chat_id, text) {
|
||||
this.broadcastToChat(chat_id, 'system_message', {
|
||||
chat_id,
|
||||
text,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Очистка старых событий
|
||||
cleanupOldEvents() {
|
||||
const now = new Date();
|
||||
const MAX_EVENT_AGE = 1 * 60 * 60 * 1000; // 1 час
|
||||
const INACTIVE_USER_THRESHOLD = 30 * 60 * 1000; // 30 минут
|
||||
|
||||
// Очищаем старые события
|
||||
this.userEventQueues.forEach((eventQueue, user_id) => {
|
||||
const filteredEvents = eventQueue.filter(event =>
|
||||
(now - event.timestamp) < MAX_EVENT_AGE
|
||||
);
|
||||
|
||||
if (filteredEvents.length !== eventQueue.length) {
|
||||
this.userEventQueues.set(user_id, filteredEvents);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем неактивных пользователей
|
||||
this.connectedClients.forEach((client, user_id) => {
|
||||
if ((now - client.lastActivity) > INACTIVE_USER_THRESHOLD) {
|
||||
this.handleClientDisconnect(user_id);
|
||||
this.userEventQueues.delete(user_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Тестирование Real-time подписки
|
||||
async testRealtimeConnection() {
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
if (!supabase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаем тестовый канал для проверки подключения
|
||||
const testChannel = supabase
|
||||
.channel('test_connection')
|
||||
.subscribe((status, error) => {
|
||||
if (error) {
|
||||
console.error('❌ [Supabase] Тестовый канал - ошибка:', error);
|
||||
}
|
||||
|
||||
if (status === 'SUBSCRIBED') {
|
||||
// Отписываемся от тестового канала
|
||||
setTimeout(() => {
|
||||
testChannel.unsubscribe();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ [Supabase] Ошибка тестирования Realtime:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка статуса подписки
|
||||
checkSubscriptionStatus() {
|
||||
if (this.realtimeSubscription) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setupRealtimeSubscription() {
|
||||
// Убираем setTimeout, вызываем сразу
|
||||
this._doSetupRealtimeSubscription();
|
||||
}
|
||||
|
||||
_doSetupRealtimeSubscription() {
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
if (!supabase) {
|
||||
console.log('❌ [Supabase] Supabase клиент не найден');
|
||||
throw new Error('Supabase client not available');
|
||||
}
|
||||
|
||||
// Подписываемся на изменения в таблице messages
|
||||
const subscription = supabase
|
||||
.channel('messages_changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'messages'
|
||||
},
|
||||
async (payload) => {
|
||||
try {
|
||||
const newMessage = payload.new;
|
||||
if (!newMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newMessage.chat_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем профиль пользователя
|
||||
const { data: userProfile, error: profileError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.eq('id', newMessage.user_id)
|
||||
.single();
|
||||
|
||||
if (profileError) {
|
||||
console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError);
|
||||
}
|
||||
|
||||
// Объединяем сообщение с профилем
|
||||
const messageWithProfile = {
|
||||
...newMessage,
|
||||
user_profiles: userProfile || null
|
||||
};
|
||||
|
||||
// Отправляем сообщение всем участникам чат
|
||||
this.broadcastToChat(newMessage.chat_id, 'new_message', {
|
||||
message: messageWithProfile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// === ЗАПУСК МОДЕРАЦИИ ===
|
||||
if (MODERATION_CONFIG.MODERATION_ENABLED) {
|
||||
|
||||
if (MODERATION_CONFIG.MODERATION_DELAY === 0) {
|
||||
setImmediate(() => {
|
||||
this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id);
|
||||
});
|
||||
} else {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id);
|
||||
}, MODERATION_CONFIG.MODERATION_DELAY);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (callbackError) {
|
||||
console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError);
|
||||
console.error('❌ [Supabase] Stack trace:', callbackError.stack);
|
||||
}
|
||||
}
|
||||
)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'messages'
|
||||
},
|
||||
async (payload) => {
|
||||
try {
|
||||
const updatedMessage = payload.new;
|
||||
if (!updatedMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updatedMessage.chat_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем профиль пользователя
|
||||
const { data: userProfile, error: profileError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, full_name, avatar_url')
|
||||
.eq('id', updatedMessage.user_id)
|
||||
.single();
|
||||
|
||||
if (profileError) {
|
||||
console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError);
|
||||
}
|
||||
|
||||
// Объединяем сообщение с профилем
|
||||
const messageWithProfile = {
|
||||
...updatedMessage,
|
||||
user_profiles: userProfile || null
|
||||
};
|
||||
|
||||
// Отправляем обновление всем участникам чат
|
||||
this.broadcastToChat(updatedMessage.chat_id, 'message_updated', {
|
||||
message: messageWithProfile,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
} catch (callbackError) {
|
||||
console.error('❌ [Supabase] Ошибка в обработчике обновления сообщения:', callbackError);
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe((status, error) => {
|
||||
if (error) {
|
||||
console.error('❌ [Supabase] Ошибка подписки:', error);
|
||||
}
|
||||
|
||||
if (status === 'CHANNEL_ERROR') {
|
||||
console.error('❌ [Supabase] Ошибка канала');
|
||||
} else if (status === 'TIMED_OUT') {
|
||||
console.error('❌ [Supabase] Таймаут подписки');
|
||||
}
|
||||
});
|
||||
|
||||
// Сохраняем ссылку на подписку для возможности отписки
|
||||
this.realtimeSubscription = subscription;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [Supabase] Критическая ошибка при настройке подписки:', error);
|
||||
throw error; // Пробрасываем ошибку для обработки в initializeWithRetry
|
||||
}
|
||||
}
|
||||
|
||||
// Функция отложенной модерации сообщения
|
||||
async moderateMessage(messageId, messageText, chatId) {
|
||||
const moderationStartTime = Date.now();
|
||||
|
||||
try {
|
||||
|
||||
// Вызываем функцию модерации
|
||||
|
||||
let comment, isApproved, finalMessage;
|
||||
const GIGA_AUTH = await getGigaKey();
|
||||
console.log(GIGA_AUTH)
|
||||
try {
|
||||
const result = await moderationText('', messageText, GIGA_AUTH);
|
||||
[comment, isApproved, finalMessage] = result;
|
||||
} catch (moderationError) {
|
||||
console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError);
|
||||
console.error(`❌ [Moderation] Stack trace:`, moderationError.stack);
|
||||
// В случае ошибки одобряем сообщение
|
||||
comment = '';
|
||||
isApproved = true;
|
||||
finalMessage = messageText;
|
||||
console.log(`⚠️ [Moderation] Используем fallback значения из-за ошибки`);
|
||||
}
|
||||
|
||||
const moderationTime = Date.now() - moderationStartTime;
|
||||
|
||||
if (isApproved) {
|
||||
console.log(`📝 [Moderation] Действие: сообщение остается без изменений`);
|
||||
} else {
|
||||
console.log(`📝 [Moderation] Действие: сообщение будет заменено в базе данных`);
|
||||
}
|
||||
|
||||
// Если сообщение не прошло модерацию, обновляем его в базе данных
|
||||
if (!isApproved) {
|
||||
console.log(`💾 [Moderation] Начинаем обновление сообщения в базе данных...`);
|
||||
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
// Сначала получаем информацию о сообщении для получения chat_id
|
||||
console.log(`💾 [Moderation] Получаем данные сообщения из базы...`);
|
||||
const { data: messageData, error: fetchError } = await supabase
|
||||
.from('messages')
|
||||
.select('chat_id, user_id')
|
||||
.eq('id', messageId)
|
||||
.single();
|
||||
|
||||
if (fetchError) {
|
||||
console.error(`❌ [Moderation] Ошибка получения данных сообщения ${messageId}:`, fetchError);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`💾 [Moderation] Данные получены. Chat ID: ${messageData.chat_id}, User ID: ${messageData.user_id}`);
|
||||
|
||||
// Обновляем текст сообщения
|
||||
console.log(`💾 [Moderation] Обновляем текст сообщения на: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}"`);
|
||||
const { data: updatedMessage, error } = await supabase
|
||||
.from('messages')
|
||||
.update({ text: MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT })
|
||||
.eq('id', messageId)
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error);
|
||||
console.error(`❌ [Moderation] Детали ошибки:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
const totalTime = Date.now() - moderationStartTime;
|
||||
console.error(`❌ [Moderation] === ОШИБКА МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`);
|
||||
console.error(`❌ [Moderation] Время до ошибки: ${totalTime}мс`);
|
||||
console.error(`❌ [Moderation] Тип ошибки: ${error.name || 'Unknown'}`);
|
||||
console.error(`❌ [Moderation] Сообщение ошибки: ${error.message || 'Unknown error'}`);
|
||||
console.error(`❌ [Moderation] Stack trace:`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Получение статистики подключений
|
||||
getConnectionStats() {
|
||||
return {
|
||||
connectedClients: this.connectedClients.size,
|
||||
activeChats: this.chatParticipants.size,
|
||||
totalChatParticipants: Array.from(this.chatParticipants.values())
|
||||
.reduce((total, participants) => total + participants.size, 0),
|
||||
totalEventQueues: this.userEventQueues.size,
|
||||
totalEvents: Array.from(this.userEventQueues.values())
|
||||
.reduce((total, queue) => total + queue.length, 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для создания роутера с polling эндпоинтами
|
||||
function createChatPollingRouter(express) {
|
||||
const router = express.Router();
|
||||
const chatHandler = new ChatPollingHandler();
|
||||
|
||||
// CORS middleware для всех запросов
|
||||
router.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Authorization');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
// Обрабатываем OPTIONS запросы
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Эндпоинт для аутентификации
|
||||
router.post('/auth', (req, res) => {
|
||||
chatHandler.handleAuthentication(req, res);
|
||||
});
|
||||
|
||||
// Эндпоинт для получения событий (polling)
|
||||
router.get('/events', (req, res) => {
|
||||
chatHandler.handleGetEvents(req, res);
|
||||
});
|
||||
|
||||
// HTTP эндпоинты для действий
|
||||
router.post('/join-chat', (req, res) => {
|
||||
chatHandler.handleJoinChat(req, res);
|
||||
});
|
||||
|
||||
router.post('/leave-chat', (req, res) => {
|
||||
chatHandler.handleLeaveChat(req, res);
|
||||
});
|
||||
|
||||
router.post('/send-message', (req, res) => {
|
||||
chatHandler.handleSendMessage(req, res);
|
||||
});
|
||||
|
||||
router.post('/typing-start', (req, res) => {
|
||||
chatHandler.handleTypingStart(req, res);
|
||||
});
|
||||
|
||||
router.post('/typing-stop', (req, res) => {
|
||||
chatHandler.handleTypingStop(req, res);
|
||||
});
|
||||
|
||||
// Эндпоинт для получения онлайн пользователей в чате
|
||||
router.get('/online-users/:chat_id', (req, res) => {
|
||||
const { chat_id } = req.params;
|
||||
const onlineUsers = chatHandler.getOnlineUsersInChat(chat_id);
|
||||
res.json({ onlineUsers });
|
||||
});
|
||||
|
||||
// Эндпоинт для получения статистики
|
||||
router.get('/stats', (req, res) => {
|
||||
const stats = chatHandler.getConnectionStats();
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// Эндпоинт для проверки статуса Supabase подписки
|
||||
router.get('/supabase-status', (req, res) => {
|
||||
const isConnected = chatHandler.checkSubscriptionStatus();
|
||||
res.json({
|
||||
supabaseSubscriptionActive: isConnected,
|
||||
subscriptionExists: !!chatHandler.realtimeSubscription,
|
||||
subscriptionInfo: chatHandler.realtimeSubscription ? {
|
||||
channel: chatHandler.realtimeSubscription.topic,
|
||||
state: chatHandler.realtimeSubscription.state
|
||||
} : null
|
||||
});
|
||||
});
|
||||
|
||||
// Эндпоинт для принудительного переподключения к Supabase
|
||||
router.post('/reconnect-supabase', (req, res) => {
|
||||
try {
|
||||
// Отписываемся от текущей подписки
|
||||
if (chatHandler.realtimeSubscription) {
|
||||
chatHandler.realtimeSubscription.unsubscribe();
|
||||
chatHandler.realtimeSubscription = null;
|
||||
}
|
||||
|
||||
// Создаем новую подписку
|
||||
chatHandler.setupRealtimeSubscription();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Reconnection initiated'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [Polling Server] Ошибка переподключения:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Reconnection failed',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для создания сообщения в обход API
|
||||
router.post('/test-message', async (req, res) => {
|
||||
const { chat_id, user_id, text } = req.body;
|
||||
|
||||
if (!chat_id || !user_id || !text) {
|
||||
res.status(400).json({ error: 'chat_id, user_id и text обязательны' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем тестовое событие напрямую
|
||||
chatHandler.broadcastToChat(chat_id, 'new_message', {
|
||||
message: {
|
||||
id: `test_${Date.now()}`,
|
||||
chat_id,
|
||||
user_id,
|
||||
text,
|
||||
created_at: new Date().toISOString(),
|
||||
user_profiles: {
|
||||
id: user_id,
|
||||
full_name: 'Test User',
|
||||
avatar_url: null
|
||||
}
|
||||
},
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Test message sent to polling clients'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [Polling Server] Ошибка отправки тестового сообщения:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send test message',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { router, chatHandler };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ChatPollingHandler,
|
||||
createChatPollingRouter
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// GET /profile
|
||||
router.get('/profile', async (req, res) => {
|
||||
const { user_id } = req.query;
|
||||
const supabase = getSupabaseClient();
|
||||
let { data: userData, error: userError } = await supabase.auth.admin.getUserById(user_id);
|
||||
|
||||
if (userError) return res.status(400).json({ error: userError.message });
|
||||
|
||||
let { data: profileData, error: profileError } = await supabase.from('user_profiles').select(`
|
||||
id,
|
||||
full_name,
|
||||
avatar_url,
|
||||
updated_at
|
||||
`).eq('id', user_id).single();
|
||||
|
||||
if (profileError) return res.status(400).json({ error: profileError.message });
|
||||
|
||||
// Получаем аватарку из бакета
|
||||
let avatarUrl = null;
|
||||
const avatarPath = `avatars/${user_id}.jpg`;
|
||||
const { data: avatarData } = await supabase.storage.from('sber.mobile').getPublicUrl(avatarPath);
|
||||
|
||||
if (avatarData) {
|
||||
// Проверяем, существует ли файл
|
||||
const { data: fileData, error: fileError } = await supabase.storage.from('sber.mobile').list('avatars', {
|
||||
search: `${user_id}.jpg`
|
||||
});
|
||||
|
||||
if (!fileError && fileData && fileData.length > 0) {
|
||||
avatarUrl = avatarData.publicUrl;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: profileData.id,
|
||||
username: profileData.full_name,
|
||||
avatar_url: avatarUrl || profileData.avatar_url,
|
||||
phone: userData.user.phone,
|
||||
updated_at: profileData.updated_at
|
||||
});
|
||||
});
|
||||
|
||||
// POST /profile
|
||||
router.post('/profile', async (req, res) => {
|
||||
const { user_id, data } = req.body;
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data: userData, error: userError } = await supabase.auth.admin.updateUserById(
|
||||
user_id,
|
||||
{ phone: data.phone }
|
||||
)
|
||||
|
||||
if (userError) return res.status(400).json({ error: userError.message });
|
||||
|
||||
let avatarUrl = data.avatar_url;
|
||||
|
||||
// Если передана аватарка в base64, сохраняем в бакет
|
||||
if (data.avatarBase64) {
|
||||
try {
|
||||
// Удаляем старую аватарку
|
||||
const oldAvatarPath = `avatars/${user_id}.jpg`;
|
||||
await supabase.storage.from('sber.mobile').remove([oldAvatarPath]);
|
||||
|
||||
// Конвертируем base64 в buffer
|
||||
const base64Data = data.avatarBase64.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// Загружаем новую аватарку
|
||||
const avatarPath = `avatars/${user_id}.jpg`;
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('sber.mobile')
|
||||
.upload(avatarPath, buffer, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.error('Ошибка загрузки аватарки:', uploadError);
|
||||
} else {
|
||||
// Получаем публичный URL
|
||||
const { data: urlData } = await supabase.storage
|
||||
.from('sber.mobile')
|
||||
.getPublicUrl(avatarPath);
|
||||
avatarUrl = urlData.publicUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка обработки аватарки:', error);
|
||||
}
|
||||
}
|
||||
|
||||
let { error: profileError } = await supabase.from('user_profiles').update({
|
||||
full_name: data.username,
|
||||
avatar_url: avatarUrl,
|
||||
// apartment: data.apartment
|
||||
}).eq('id', user_id).single();
|
||||
|
||||
if (profileError) return res.status(400).json({ error: profileError.message });
|
||||
|
||||
res.json({ success: true, avatar_url: avatarUrl });
|
||||
});
|
||||
|
||||
// Получить управляющую компанию по квартире
|
||||
router.get('/management-company', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { apartment_id } = req.query;
|
||||
if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' });
|
||||
const { data: apartment, error: err1 } = await supabase.from('apartments').select('building_id').eq('id', apartment_id).single();
|
||||
if (err1) return res.status(400).json({ error: err1.message });
|
||||
const { data: building, error: err2 } = await supabase.from('buildings').select('management_company_id').eq('id', apartment.building_id).single();
|
||||
if (err2) return res.status(400).json({ error: err2.message });
|
||||
const { data: company, error: err3 } = await supabase.from('management_companies').select('*').eq('id', building.management_company_id).single();
|
||||
if (err3) return res.status(400).json({ error: err3.message });
|
||||
res.json(company);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,79 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey } = require('./get-constants');
|
||||
|
||||
let supabase = null;
|
||||
let initializationPromise = null;
|
||||
|
||||
async function initSupabaseClient() {
|
||||
|
||||
try {
|
||||
const supabaseUrl = await getSupabaseUrl();
|
||||
const supabaseAnonKey = await getSupabaseKey();
|
||||
const supabaseServiceRoleKey = await getSupabaseServiceKey();
|
||||
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceRoleKey) {
|
||||
throw new Error('Missing required Supabase configuration');
|
||||
}
|
||||
|
||||
supabase = createClient(supabaseUrl, supabaseServiceRoleKey);
|
||||
|
||||
return supabase;
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getSupabaseClient() {
|
||||
if (!supabase) {
|
||||
throw new Error('Supabase client is not initialized. Call initSupabaseClient first.');
|
||||
}
|
||||
return supabase;
|
||||
}
|
||||
|
||||
// POST /refresh-supabase-client
|
||||
router.post('/refresh-supabase-client', async (req, res) => {
|
||||
try {
|
||||
await initSupabaseClient();
|
||||
res.json({ success: true, message: 'Supabase client refreshed' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /supabase-client-status
|
||||
router.get('/supabase-client-status', (req, res) => {
|
||||
|
||||
const isInitialized = !!supabase;
|
||||
|
||||
res.json({
|
||||
initialized: isInitialized,
|
||||
clientExists: !!supabase,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Инициализация клиента при старте
|
||||
initializationPromise = (async () => {
|
||||
try {
|
||||
await initSupabaseClient();
|
||||
} catch (error) {
|
||||
// Планируем повторную попытку через 5 секунд
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await initSupabaseClient();
|
||||
} catch (retryError) {
|
||||
console.error('❌ [Supabase Client] Повторная инициализация неудачна:', retryError);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
getSupabaseClient,
|
||||
initSupabaseClient,
|
||||
supabaseRouter: router,
|
||||
initializationPromise
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||
import { getSupabaseClient } from '../supabaseClient';
|
||||
|
||||
export class CreateTicketTool extends StructuredTool {
|
||||
name = 'create_ticket';
|
||||
description = 'Создает заявку в системе. ВАЖНО: используй этот инструмент ТОЛЬКО после получения явного согласия пользователя на создание заявки с конкретным текстом.';
|
||||
|
||||
schema = z.object({
|
||||
title: z.string().describe('Заголовок заявки'),
|
||||
description: z.string().describe('Подробное описание проблемы'),
|
||||
category: z.string().describe('Категория заявки (например: ремонт, уборка, техническая_поддержка, жалоба)'),
|
||||
});
|
||||
|
||||
private userId: string;
|
||||
private apartmentId: string;
|
||||
|
||||
constructor(userId: string, apartmentId: string) {
|
||||
super();
|
||||
this.userId = userId;
|
||||
this.apartmentId = apartmentId;
|
||||
}
|
||||
|
||||
protected async _call(
|
||||
arg: z.infer<typeof this.schema>,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
parentConfig?: ToolRunnableConfig<Record<string, any>>
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (!this.apartmentId) {
|
||||
return 'Не удалось определить вашу квартиру. Обратитесь к администратору для создания заявки.';
|
||||
}
|
||||
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data: ticket, error } = await supabase
|
||||
.from('tickets')
|
||||
.insert({
|
||||
user_id: this.userId,
|
||||
apartment_id: this.apartmentId,
|
||||
title: arg.title,
|
||||
description: arg.description,
|
||||
category: arg.category,
|
||||
status: 'open'
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return 'Произошла ошибка при создании заявки. Попробуйте позже или обратитесь к администратору.';
|
||||
}
|
||||
|
||||
return `Заявка успешно создана!
|
||||
Номер заявки: ${ticket.id}
|
||||
Заголовок: ${ticket.title}
|
||||
Статус: Открыта
|
||||
Дата создания: ${new Date(ticket.created_at).toLocaleString('ru-RU')}
|
||||
|
||||
Ваша заявка принята в работу. Мы свяжемся с вами в ближайшее время.`;
|
||||
|
||||
} catch (error) {
|
||||
return 'Произошла техническая ошибка при создании заявки. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Agent } from 'node:https';
|
||||
import { GigaChat } from 'langchain-gigachat';
|
||||
import { getGigaAuth } from '../get-constants';
|
||||
|
||||
const httpsAgent = new Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js)
|
||||
export const gigachat = (GIGA_AUTH) =>
|
||||
new GigaChat({
|
||||
model: 'GigaChat-2',
|
||||
temperature: 0.7,
|
||||
scope: 'GIGACHAT_API_PERS',
|
||||
streaming: false,
|
||||
credentials: GIGA_AUTH,
|
||||
httpsAgent
|
||||
});
|
||||
|
||||
export default gigachat;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||
import { getVectorStore } from './vector-store';
|
||||
|
||||
export class KnowledgeBaseTool extends StructuredTool {
|
||||
name = 'search_knowledge_base';
|
||||
description = 'Ищет информацию в базе знаний компании о процессах, оплатах, подаче заявок, правилах и документах УК. Используй этот инструмент для вопросов, требующих специфических знаний о компании.';
|
||||
|
||||
schema = z.object({
|
||||
query: z.string().describe('Поисковый запрос для поиска в базе знаний'),
|
||||
});
|
||||
|
||||
protected async _call(
|
||||
arg: z.infer<typeof this.schema>,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
parentConfig?: ToolRunnableConfig<Record<string, any>>
|
||||
): Promise<string> {
|
||||
try {
|
||||
const vectorStore = getVectorStore();
|
||||
const retriever = vectorStore.asRetriever({
|
||||
k: 5
|
||||
});
|
||||
|
||||
const relevantDocs = await retriever.getRelevantDocuments(arg.query);
|
||||
|
||||
if (!relevantDocs || relevantDocs.length === 0) {
|
||||
return 'В базе знаний не найдено информации по данному запросу. Возможно, стоит переформулировать вопрос или обратиться к специалисту.';
|
||||
}
|
||||
|
||||
const formattedDocs = relevantDocs.map((doc, index) => {
|
||||
return `Документ ${index + 1}:\n${doc.pageContent}\n`;
|
||||
}).join('\n---\n');
|
||||
|
||||
return `Найдена следующая информация в базе знаний компании:\n\n${formattedDocs}\n\nИспользуй эту информацию для ответа на вопрос пользователя.`;
|
||||
|
||||
} catch (error) {
|
||||
return 'Произошла ошибка при поиске в базе знаний. Попробуйте переформулировать запрос.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from '@langchain/core/messages';
|
||||
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
|
||||
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import gigachat from './gigachat';
|
||||
import { SupportContextTool } from './support-context-tool';
|
||||
import { KnowledgeBaseTool } from './knowledge-base-tool';
|
||||
import { CreateTicketTool } from './create-ticket-tool';
|
||||
|
||||
export interface SupportAgentConfig {
|
||||
temperature?: number;
|
||||
threadId?: string;
|
||||
GIGA_AUTH?: string;
|
||||
}
|
||||
|
||||
export interface SupportResponse {
|
||||
content: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class SupportAgent {
|
||||
private llm: any;
|
||||
private memorySaver: MemorySaver;
|
||||
private agent: any;
|
||||
private systemPrompt: string;
|
||||
private threadId: string;
|
||||
private isFirstMessage: boolean;
|
||||
private userId: string;
|
||||
|
||||
constructor(config: SupportAgentConfig = {}) {
|
||||
this.systemPrompt = this.getDefaultSystemPrompt();
|
||||
this.threadId = config.threadId || 'default';
|
||||
this.userId = this.threadId;
|
||||
this.memorySaver = new MemorySaver();
|
||||
this.isFirstMessage = true;
|
||||
|
||||
this.llm = gigachat(config.GIGA_AUTH);
|
||||
if (config.temperature !== undefined) {
|
||||
this.llm.temperature = config.temperature;
|
||||
}
|
||||
|
||||
const tools = [
|
||||
new SupportContextTool(this.userId),
|
||||
new KnowledgeBaseTool()
|
||||
];
|
||||
|
||||
this.agent = createReactAgent({
|
||||
llm: this.llm,
|
||||
tools: tools,
|
||||
checkpointSaver: this.memorySaver
|
||||
});
|
||||
}
|
||||
|
||||
private getDefaultSystemPrompt(): string {
|
||||
return `Ты - профессиональный агент службы поддержки управляющей компании.
|
||||
|
||||
ОСНОВНЫЕ ПРИНЦИПЫ:
|
||||
- Помогай только с реальными проблемами и вопросами, связанными с ЖКХ, управляющей компанией и приложением
|
||||
- Будь вежливым, профессиональным и по существу
|
||||
- Если вопрос неуместен, не связан с твоими обязанностями или является развлекательным - вежливо откажись и перенаправь к основным темам
|
||||
|
||||
ДОСТУПНЫЕ ИНСТРУМЕНТЫ:
|
||||
|
||||
1. get_support_context - получает историю сообщений пользователя
|
||||
ВСЕГДА используй ПЕРВЫМ при каждом новом сообщении
|
||||
|
||||
2. search_knowledge_base - поиск в базе знаний компании
|
||||
Используй ТОЛЬКО для серьезных вопросов о:
|
||||
- Процессах оплаты ЖКХ и тарифах
|
||||
- Подаче заявок и документообороте
|
||||
- Правилах и регламентах УК
|
||||
- Технических вопросах приложения
|
||||
- Процедурах и инструкциях компании
|
||||
|
||||
3. create_ticket - создание заявки в системе
|
||||
Используй ТОЛЬКО когда:
|
||||
- Пользователь сообщает о реальной проблеме (поломка, неисправность, жалоба)
|
||||
- Проблема требует вмешательства УК или технических служб
|
||||
- ОБЯЗАТЕЛЬНО сначала покажи пользователю полный текст заявки
|
||||
- Получи ЯВНОЕ согласие пользователя перед созданием
|
||||
- НЕ создавай заявки для консультационных вопросов
|
||||
|
||||
ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ:
|
||||
- НЕ используй search_knowledge_base и create_ticket для:
|
||||
* Общих вопросов и болтовни
|
||||
* Развлекательных запросов
|
||||
* Вопросов не по теме ЖКХ/УК
|
||||
* Простых консультаций, которые можно решить обычным ответом
|
||||
|
||||
АЛГОРИТМ РАБОТЫ:
|
||||
1. Получи контекст истории сообщений
|
||||
2. Определи, является ли вопрос уместным и серьезным
|
||||
3. Если нужна специфическая информация - найди в базе знаний
|
||||
4. Если нужно создать заявку - покажи текст и получи согласие
|
||||
5. Дай полный и полезный ответ
|
||||
|
||||
Всегда отвечай на русском языке и фокусируйся на помощи с реальными проблемами ЖКХ.`;
|
||||
}
|
||||
|
||||
public async processMessage(userMessage: string, apartmentId?: string): Promise<SupportResponse> {
|
||||
try {
|
||||
const messages: BaseMessage[] = [];
|
||||
|
||||
if (this.isFirstMessage) {
|
||||
messages.push(new SystemMessage(this.systemPrompt));
|
||||
this.isFirstMessage = false;
|
||||
}
|
||||
|
||||
messages.push(new HumanMessage(userMessage));
|
||||
|
||||
// Создаем инструменты с актуальным apartmentId
|
||||
const tools = [
|
||||
new SupportContextTool(this.userId),
|
||||
new KnowledgeBaseTool(),
|
||||
new CreateTicketTool(this.userId, apartmentId || '')
|
||||
];
|
||||
|
||||
// Пересоздаем агента с обновленными инструментами
|
||||
const tempAgent = createReactAgent({
|
||||
llm: this.llm,
|
||||
tools: tools,
|
||||
checkpointSaver: this.memorySaver
|
||||
});
|
||||
|
||||
const response = await tempAgent.invoke({
|
||||
messages: messages
|
||||
}, {
|
||||
configurable: {
|
||||
thread_id: this.threadId
|
||||
}
|
||||
});
|
||||
|
||||
const lastMessage = response.messages[response.messages.length - 1];
|
||||
|
||||
return {
|
||||
content: typeof lastMessage.content === 'string' ? lastMessage.content : 'Извините, не удалось сформировать ответ.',
|
||||
success: true
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обработке сообщения:', error);
|
||||
return {
|
||||
content: 'Извините, произошла ошибка при обработке вашего запроса. Попробуйте позже.',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async clearHistory(): Promise<void> {
|
||||
this.memorySaver = new MemorySaver();
|
||||
|
||||
const tools = [
|
||||
new SupportContextTool(this.userId),
|
||||
new KnowledgeBaseTool()
|
||||
];
|
||||
|
||||
this.agent = createReactAgent({
|
||||
llm: this.llm,
|
||||
tools: tools,
|
||||
checkpointSaver: this.memorySaver
|
||||
});
|
||||
|
||||
this.isFirstMessage = true;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||
import { getSupabaseClient } from '../supabaseClient';
|
||||
|
||||
export class SupportContextTool extends StructuredTool {
|
||||
name = 'get_support_context';
|
||||
description = 'Получает последние 10 сообщений из истории поддержки для понимания контекста разговора. Используй этот инструмент в начале разговора.';
|
||||
|
||||
schema = z.object({});
|
||||
|
||||
private userId: string;
|
||||
|
||||
constructor(userId: string) {
|
||||
super();
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
protected async _call(
|
||||
arg: z.infer<typeof this.schema>,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
parentConfig?: ToolRunnableConfig<Record<string, any>>
|
||||
): Promise<string> {
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data: messages, error } = await supabase
|
||||
.from('support')
|
||||
.select('message, is_from_user, created_at')
|
||||
.eq('user_id', this.userId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (error) {
|
||||
return 'Не удалось получить историю сообщений.';
|
||||
}
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
return 'История сообщений поддержки пуста. Это первое обращение пользователя.';
|
||||
}
|
||||
|
||||
const chronologicalMessages = messages.reverse();
|
||||
|
||||
const contextMessages = chronologicalMessages.map((msg, index) => {
|
||||
const role = msg.is_from_user ? 'Пользователь' : 'Агент поддержки';
|
||||
const time = new Date(msg.created_at).toLocaleString('ru-RU');
|
||||
return `${index + 1}. [${time}] ${role}: ${msg.message}`;
|
||||
}).join('\n');
|
||||
|
||||
return `Последние сообщения из истории поддержки (${messages.length} сообщений):\n\n${contextMessages}\n\nИспользуй этот контекст для понимания предыдущих обращений пользователя и предоставления более точных ответов.`;
|
||||
|
||||
} catch (error) {
|
||||
return 'Произошла ошибка при получении истории сообщений.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase';
|
||||
import { GigaChatEmbeddings } from 'langchain-gigachat';
|
||||
import { Agent } from 'node:https';
|
||||
|
||||
const httpsAgent = new Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
let vectorStoreInstance: SupabaseVectorStore | null = null;
|
||||
|
||||
export function getVectorStore(): SupabaseVectorStore {
|
||||
if (!vectorStoreInstance) {
|
||||
const client = createClient(
|
||||
process.env.RAG_SUPABASE_URL!,
|
||||
process.env.RAG_SUPABASE_SERVICE_ROLE_KEY!,
|
||||
);
|
||||
|
||||
vectorStoreInstance = new SupabaseVectorStore(
|
||||
new GigaChatEmbeddings({
|
||||
credentials: process.env.GIGA_AUTH,
|
||||
httpsAgent,
|
||||
}),
|
||||
{
|
||||
client,
|
||||
tableName: 'slon',
|
||||
queryName: 'match_slon'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return vectorStoreInstance;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
const { getGigaAuth } = require('./get-constants');
|
||||
const { SupportAgent } = require('./support-ai-agent/support-agent');
|
||||
|
||||
// Хранилище агентов для разных пользователей
|
||||
const userAgents = new Map();
|
||||
|
||||
/**
|
||||
* Получить или создать агента для пользователя
|
||||
*/
|
||||
async function getUserAgent(userId) {
|
||||
if (!userAgents.has(userId)) {
|
||||
const GIGA_AUTH = await getGigaAuth();
|
||||
const config = {
|
||||
threadId: userId,
|
||||
temperature: 0.7,
|
||||
GIGA_AUTH
|
||||
};
|
||||
userAgents.set(userId, new SupportAgent(config));
|
||||
}
|
||||
return userAgents.get(userId);
|
||||
}
|
||||
|
||||
// GET /api/support - Получить историю сообщений пользователя
|
||||
router.get('/support', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { user_id } = req.query;
|
||||
|
||||
if (!user_id) {
|
||||
return res.status(400).json({ error: 'user_id обязателен' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем все сообщения пользователя из базы данных
|
||||
const { data: messages, error } = await supabase
|
||||
.from('support')
|
||||
.select('*')
|
||||
.eq('user_id', user_id)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
res.json({
|
||||
messages: messages || [],
|
||||
success: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка в GET /support:', error);
|
||||
res.status(500).json({
|
||||
error: 'Внутренняя ошибка сервера',
|
||||
success: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/support
|
||||
router.post('/support', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { user_id, message, apartment_id } = req.body;
|
||||
|
||||
if (!user_id || !message) {
|
||||
return res.status(400).json({ error: 'user_id и message обязательны' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Сохраняем сообщение пользователя в базу данных
|
||||
const { error: insertError } = await supabase
|
||||
.from('support')
|
||||
.insert({ user_id, message, is_from_user: true });
|
||||
|
||||
if (insertError) {
|
||||
return res.status(400).json({ error: insertError.message });
|
||||
}
|
||||
|
||||
// Получаем агента для пользователя
|
||||
const agent = await getUserAgent(user_id);
|
||||
|
||||
// Получаем ответ от AI-агента, передавая apartment_id
|
||||
const aiResponse = await agent.processMessage(message, apartment_id);
|
||||
|
||||
if (!aiResponse.success) {
|
||||
console.error('Ошибка AI-агента:', aiResponse.error);
|
||||
return res.status(500).json({
|
||||
error: 'Ошибка при генерации ответа',
|
||||
reply: 'Извините, произошла ошибка. Попробуйте позже.'
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем ответ агента в базу данных
|
||||
const { error: responseError } = await supabase
|
||||
.from('support')
|
||||
.insert({
|
||||
user_id,
|
||||
message: aiResponse.content,
|
||||
is_from_user: false
|
||||
});
|
||||
|
||||
if (responseError) {
|
||||
console.error('Ошибка сохранения ответа:', responseError);
|
||||
// Не возвращаем ошибку пользователю, так как ответ уже сгенерирован
|
||||
}
|
||||
|
||||
// Возвращаем ответ пользователю
|
||||
res.json({
|
||||
reply: aiResponse.content,
|
||||
success: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка в supportApi:', error);
|
||||
res.status(500).json({
|
||||
error: 'Внутренняя ошибка сервера',
|
||||
reply: 'Извините, произошла ошибка. Попробуйте позже.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/support/history/:userId - Очистка истории диалога
|
||||
router.delete('/support/history/:userId', async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
try {
|
||||
if (userAgents.has(userId)) {
|
||||
const agent = userAgents.get(userId);
|
||||
await agent.clearHistory();
|
||||
|
||||
res.json({
|
||||
message: 'История диалога очищена',
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
message: 'Агент для данного пользователя не найден',
|
||||
success: true
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка в /support/history:', error);
|
||||
res.status(500).json({
|
||||
error: 'Внутренняя ошибка сервера',
|
||||
success: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,31 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить заявки пользователя по квартире
|
||||
router.get('/tickets', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { user_id, apartment_id } = req.query;
|
||||
|
||||
if (!user_id || !apartment_id) {
|
||||
return res.status(400).json({ error: 'Требуется user_id и apartment_id' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('tickets')
|
||||
.select('*')
|
||||
.eq('user_id', user_id)
|
||||
.eq('apartment_id', apartment_id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
res.json(data || []);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,18 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все квартиры пользователя
|
||||
router.get('/user-apartments', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { user_id } = req.query;
|
||||
if (!user_id) return res.status(400).json({ error: 'user_id required' });
|
||||
const { data: links, error: err1 } = await supabase.from('apartment_residents').select('apartment_id').eq('user_id', user_id);
|
||||
if (err1) return res.status(400).json({ error: err1.message });
|
||||
const apartmentIds = links.map(l => l.apartment_id);
|
||||
if (!apartmentIds.length) return res.json([]);
|
||||
const { data, error } = await supabase.from('apartments').select('*').in('id', apartmentIds);
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,50 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить платежки с деталями для квартиры
|
||||
router.get('/payment-services', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { apartment_id } = req.query;
|
||||
if (!apartment_id) return res.status(400).json({ error: 'apartment_id обязателен' });
|
||||
|
||||
// Получаем все платежки по квартире
|
||||
const { data: services, error: servicesError } = await supabase
|
||||
.from('payment_services')
|
||||
.select('id, name, icon, amount, is_paid, payment_method')
|
||||
.eq('apartment_id', apartment_id);
|
||||
if (servicesError) return res.status(400).json({ error: servicesError.message });
|
||||
|
||||
// Получаем детализацию по всем платежкам
|
||||
const serviceIds = services.map(s => s.id);
|
||||
let details = [];
|
||||
if (serviceIds.length > 0) {
|
||||
const { data: detailsData, error: detailsError } = await supabase
|
||||
.from('payment_service_details')
|
||||
.select('id, payment_service_id, name, amount')
|
||||
.in('payment_service_id', serviceIds);
|
||||
if (detailsError) return res.status(400).json({ error: detailsError.message });
|
||||
details = detailsData;
|
||||
}
|
||||
|
||||
// Формируем структуру для фронта
|
||||
const result = services.map(service => {
|
||||
const serviceDetails = details.filter(d => d.payment_service_id === service.id).map(detail => ({
|
||||
id: detail.id,
|
||||
name: detail.name,
|
||||
amount: detail.amount
|
||||
}));
|
||||
return {
|
||||
id: service.id,
|
||||
title: service.name,
|
||||
icon: service.icon,
|
||||
amount: service.amount,
|
||||
isPaid: service.is_paid,
|
||||
paymentMethod: service.payment_method,
|
||||
details: serviceDetails
|
||||
};
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,105 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { getSupabaseClient } = require('./supabaseClient');
|
||||
|
||||
// Получить все голоса по инициативе
|
||||
router.get('/votes/:initiative_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { initiative_id } = req.params;
|
||||
const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id);
|
||||
if (error)
|
||||
return res.status(400).json({ error: error.message });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить голос пользователя по инициативе
|
||||
router.get('/votes/:initiative_id/user/:user_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { initiative_id, user_id } = req.params;
|
||||
const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id).eq('user_id', user_id).single();
|
||||
if (error) {
|
||||
console.log(error, '/votes/:initiative_id/:user_id')
|
||||
console.log(initiative_id, user_id)
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
// Получить статистику голосов по инициативе
|
||||
router.get('/votes/stats/:initiative_id', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { initiative_id } = req.params;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('votes')
|
||||
.select('vote_type')
|
||||
.eq('initiative_id', initiative_id);
|
||||
console.log(data, error)
|
||||
if (error) {
|
||||
console.log('/votes/:initiative_id/stats')
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
const stats = {
|
||||
for: data.filter(vote => vote.vote_type === 'for').length,
|
||||
against: data.filter(vote => vote.vote_type === 'against').length,
|
||||
total: data.length
|
||||
};
|
||||
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// Проголосовать (создать, обновить или удалить голос)
|
||||
router.post('/votes', async (req, res) => {
|
||||
const supabase = getSupabaseClient();
|
||||
const { initiative_id, user_id, vote_type } = req.body;
|
||||
|
||||
// Проверяем существующий голос
|
||||
const { data: existingVote, error: checkError } = await supabase
|
||||
.from('votes')
|
||||
.select('*')
|
||||
.eq('initiative_id', initiative_id)
|
||||
.eq('user_id', user_id)
|
||||
.single();
|
||||
|
||||
if (checkError && checkError.code !== 'PGRST116') {
|
||||
console.log('1/votes')
|
||||
return res.status(400).json({ error: checkError.message });
|
||||
}
|
||||
|
||||
if (existingVote) {
|
||||
if (existingVote.vote_type === vote_type) {
|
||||
// Если нажали тот же тип голоса - УДАЛЯЕМ (отменяем голос)
|
||||
const { error: deleteError } = await supabase
|
||||
.from('votes')
|
||||
.delete()
|
||||
.eq('initiative_id', initiative_id)
|
||||
.eq('user_id', user_id);
|
||||
|
||||
if (deleteError) return res.status(400).json({ error: deleteError.message });
|
||||
res.json({ message: 'Vote removed', action: 'removed', previous_vote: existingVote.vote_type });
|
||||
} else {
|
||||
// Если нажали другой тип голоса - ОБНОВЛЯЕМ
|
||||
const { data, error } = await supabase
|
||||
.from('votes')
|
||||
.update({ vote_type })
|
||||
.eq('initiative_id', initiative_id)
|
||||
.eq('user_id', user_id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ ...data, action: 'updated', previous_vote: existingVote.vote_type });
|
||||
}
|
||||
} else {
|
||||
// Если голоса нет - СОЗДАЕМ новый
|
||||
const { data, error } = await supabase
|
||||
.from('votes')
|
||||
.insert([{ initiative_id, user_id, vote_type }])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return res.status(400).json({ error: error.message });
|
||||
res.json({ ...data, action: 'created' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user