ваше сообщение коммита
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/5f658ec7c83a39083e0b58539865c835.json"
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/5f658ec7c83a39083e0b58539865c835.json"
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
||||
"buildInfo": "../../build-info/362ff3981c938c72363f6427a454b84b.json"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
171
backend/cache/solidity-files-cache.json
vendored
171
backend/cache/solidity-files-cache.json
vendored
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"_format": "hh-sol-cache-2",
|
||||
"files": {
|
||||
"/app/contracts/DLE.sol": {
|
||||
"lastModificationDate": 1753802664167,
|
||||
"contentHash": "de19ae5d6875c4b57e17312ebe37ae43",
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/DLE.sol": {
|
||||
"lastModificationDate": 1754485037554,
|
||||
"contentHash": "f121cb518877db715ab5cd2e3ee5ff3a",
|
||||
"sourceName": "contracts/DLE.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
@@ -32,7 +32,8 @@
|
||||
},
|
||||
"imports": [
|
||||
"@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
||||
"@openzeppelin/contracts/utils/ReentrancyGuard.sol"
|
||||
"@openzeppelin/contracts/utils/ReentrancyGuard.sol",
|
||||
"@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"
|
||||
],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
@@ -41,44 +42,7 @@
|
||||
"DLE"
|
||||
]
|
||||
},
|
||||
"/app/node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol": {
|
||||
"lastModificationDate": 1753876422645,
|
||||
"contentHash": "190613e556d509d9e9a0ea43dc5d891d",
|
||||
"sourceName": "@openzeppelin/contracts/utils/ReentrancyGuard.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"evmVersion": "paris",
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
],
|
||||
"artifacts": [
|
||||
"ReentrancyGuard"
|
||||
]
|
||||
},
|
||||
"/app/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol": {
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol": {
|
||||
"lastModificationDate": 1754306764456,
|
||||
"contentHash": "227a6eb2225701c12d9c959b758b6333",
|
||||
"sourceName": "@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
||||
@@ -120,8 +84,45 @@
|
||||
"ERC20"
|
||||
]
|
||||
},
|
||||
"/app/node_modules/@openzeppelin/contracts/utils/Context.sol": {
|
||||
"lastModificationDate": 1753876422645,
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol": {
|
||||
"lastModificationDate": 1754306760451,
|
||||
"contentHash": "190613e556d509d9e9a0ea43dc5d891d",
|
||||
"sourceName": "@openzeppelin/contracts/utils/ReentrancyGuard.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"evmVersion": "paris",
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
],
|
||||
"artifacts": [
|
||||
"ReentrancyGuard"
|
||||
]
|
||||
},
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/Context.sol": {
|
||||
"lastModificationDate": 1754306760451,
|
||||
"contentHash": "67bfbc07588eb8683b3fd8f6f909563e",
|
||||
"sourceName": "@openzeppelin/contracts/utils/Context.sol",
|
||||
"solcConfig": {
|
||||
@@ -157,7 +158,7 @@
|
||||
"Context"
|
||||
]
|
||||
},
|
||||
"/app/node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol": {
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol": {
|
||||
"lastModificationDate": 1754306760460,
|
||||
"contentHash": "267d92fe4de67b1bdb3302c08f387dbf",
|
||||
"sourceName": "@openzeppelin/contracts/interfaces/draft-IERC6093.sol",
|
||||
@@ -196,7 +197,7 @@
|
||||
"IERC721Errors"
|
||||
]
|
||||
},
|
||||
"/app/node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol": {
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol": {
|
||||
"lastModificationDate": 1754306764456,
|
||||
"contentHash": "8f19f64d2adadf448840908bbaf431c8",
|
||||
"sourceName": "@openzeppelin/contracts/token/ERC20/IERC20.sol",
|
||||
@@ -233,7 +234,7 @@
|
||||
"IERC20"
|
||||
]
|
||||
},
|
||||
"/app/node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
|
||||
"lastModificationDate": 1754306768254,
|
||||
"contentHash": "794db3115001aa372c79326fcfd44b1f",
|
||||
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
|
||||
@@ -271,6 +272,82 @@
|
||||
"artifacts": [
|
||||
"IERC20Metadata"
|
||||
]
|
||||
},
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/cryptography/MerkleProof.sol": {
|
||||
"lastModificationDate": 1754306764465,
|
||||
"contentHash": "d57b0dba03e8cc7942bf797fc9fe1d29",
|
||||
"sourceName": "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"evmVersion": "paris",
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"./Hashes.sol"
|
||||
],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
],
|
||||
"artifacts": [
|
||||
"MerkleProof"
|
||||
]
|
||||
},
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/cryptography/Hashes.sol": {
|
||||
"lastModificationDate": 1754306764456,
|
||||
"contentHash": "34f1345e1a955860b49b83bf791500a6",
|
||||
"sourceName": "@openzeppelin/contracts/utils/cryptography/Hashes.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"evmVersion": "paris",
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
],
|
||||
"artifacts": [
|
||||
"Hashes"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
||||
|
||||
/**
|
||||
* @title DLE (Digital Legal Entity)
|
||||
@@ -59,36 +60,29 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
mapping(uint256 => bool) chainVoteSynced; // Синхронизация голосов между цепочками
|
||||
}
|
||||
|
||||
struct MultiSigOperation {
|
||||
bytes32 operationHash;
|
||||
uint256 forSignatures;
|
||||
uint256 againstSignatures;
|
||||
bool executed;
|
||||
uint256 deadline;
|
||||
address initiator;
|
||||
mapping(address => bool) hasSigned;
|
||||
mapping(uint256 => bool) chainSignSynced; // Синхронизация подписей между цепочками
|
||||
}
|
||||
|
||||
|
||||
// Основные настройки
|
||||
DLEInfo public dleInfo;
|
||||
uint256 public quorumPercentage;
|
||||
uint256 public proposalCounter;
|
||||
uint256 public multiSigCounter;
|
||||
uint256 public currentChainId;
|
||||
|
||||
// Модули
|
||||
mapping(bytes32 => address) public modules;
|
||||
mapping(bytes32 => bool) public activeModules;
|
||||
|
||||
// Предложения и мультиподписи
|
||||
// Предложения
|
||||
mapping(uint256 => Proposal) public proposals;
|
||||
mapping(uint256 => MultiSigOperation) public multiSigOperations;
|
||||
|
||||
// Мульти-чейн
|
||||
mapping(uint256 => bool) public supportedChains;
|
||||
uint256[] public supportedChainIds;
|
||||
mapping(uint256 => bool) public executedProposals; // Синхронизация исполненных предложений
|
||||
mapping(uint256 => bool) public executedMultiSig; // Синхронизация исполненных мультиподписей
|
||||
|
||||
// Merkle proofs для cross-chain синхронизации
|
||||
mapping(uint256 => bytes32) public chainMerkleRoots; // chainId => merkleRoot
|
||||
mapping(uint256 => mapping(uint256 => bool)) public processedProofs; // proposalId => proofHash => processed
|
||||
|
||||
// События
|
||||
event DLEInitialized(
|
||||
@@ -107,14 +101,16 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
event ProposalCreated(uint256 proposalId, address initiator, string description);
|
||||
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
|
||||
event ProposalExecuted(uint256 proposalId, bytes operation);
|
||||
event MultiSigOperationCreated(uint256 operationId, address initiator, bytes32 operationHash);
|
||||
event MultiSigSigned(uint256 operationId, address signer, bool support, uint256 signaturePower);
|
||||
event MultiSigExecuted(uint256 operationId, bytes32 operationHash);
|
||||
event ModuleAdded(bytes32 moduleId, address moduleAddress);
|
||||
event ModuleRemoved(bytes32 moduleId);
|
||||
event CrossChainExecutionSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
|
||||
event CrossChainVoteSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
|
||||
event CrossChainMultiSigSync(uint256 operationId, uint256 fromChainId, uint256 toChainId);
|
||||
event ChainAdded(uint256 chainId);
|
||||
event ChainRemoved(uint256 chainId);
|
||||
event ChainMerkleRootSet(uint256 chainId, bytes32 merkleRoot);
|
||||
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp);
|
||||
event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
|
||||
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
|
||||
|
||||
constructor(
|
||||
DLEConfig memory config,
|
||||
@@ -139,6 +135,7 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
// Настраиваем поддерживаемые цепочки
|
||||
for (uint256 i = 0; i < config.supportedChainIds.length; i++) {
|
||||
supportedChains[config.supportedChainIds[i]] = true;
|
||||
supportedChainIds.push(config.supportedChainIds[i]);
|
||||
}
|
||||
|
||||
// Распределяем начальные токены партнерам
|
||||
@@ -237,15 +234,34 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
uint256 _fromChainId,
|
||||
uint256 _forVotes,
|
||||
uint256 _againstVotes,
|
||||
bytes memory /* _proof */
|
||||
bytes memory _proof
|
||||
) external {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(supportedChains[_fromChainId], "Chain not supported");
|
||||
require(!proposal.chainVoteSynced[_fromChainId], "Already synced");
|
||||
|
||||
// Здесь должна быть проверка proof (для простоты пропускаем)
|
||||
// В реальной реализации нужно проверять доказательство
|
||||
// Проверяем доказательство cross-chain синхронизации
|
||||
require(_proof.length > 0, "Proof required for cross-chain sync");
|
||||
|
||||
// Проверяем Merkle proof для cross-chain синхронизации
|
||||
bytes32 proofHash = keccak256(abi.encodePacked(_proposalId, _fromChainId, _forVotes, _againstVotes));
|
||||
require(!processedProofs[_proposalId][uint256(proofHash)], "Proof already processed");
|
||||
|
||||
// Проверяем, что Merkle root для цепочки установлен
|
||||
bytes32 merkleRoot = chainMerkleRoots[_fromChainId];
|
||||
require(merkleRoot != bytes32(0), "Merkle root not set for chain");
|
||||
|
||||
// Проверяем Merkle proof
|
||||
bytes32[] memory proof = abi.decode(_proof, (bytes32[]));
|
||||
require(MerkleProof.verify(proof, merkleRoot, proofHash), "Invalid Merkle proof");
|
||||
|
||||
// Отмечаем proof как обработанный
|
||||
processedProofs[_proposalId][uint256(proofHash)] = true;
|
||||
|
||||
// Проверяем, что голоса не превышают общее количество токенов
|
||||
uint256 totalVotes = _forVotes + _againstVotes;
|
||||
require(totalVotes <= totalSupply(), "Votes exceed total supply");
|
||||
|
||||
proposal.forVotes += _forVotes;
|
||||
proposal.againstVotes += _againstVotes;
|
||||
@@ -281,9 +297,15 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp >= proposal.deadline, "Voting not ended");
|
||||
|
||||
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
||||
|
||||
// Предложение можно выполнить если:
|
||||
// 1. Дедлайн истек ИЛИ кворум достигнут
|
||||
require(
|
||||
block.timestamp >= proposal.deadline || quorumReached,
|
||||
"Voting not ended and quorum not reached"
|
||||
);
|
||||
require(passed && quorumReached, "Proposal not passed");
|
||||
|
||||
proposal.executed = true;
|
||||
@@ -294,123 +316,6 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать мультиподпись операцию
|
||||
* @param _operationHash Хеш операции
|
||||
* @param _duration Длительность сбора подписей
|
||||
*/
|
||||
function createMultiSigOperation(
|
||||
bytes32 _operationHash,
|
||||
uint256 _duration
|
||||
) external returns (uint256) {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create operation");
|
||||
require(_duration > 0, "Duration must be positive");
|
||||
|
||||
uint256 operationId = multiSigCounter++;
|
||||
MultiSigOperation storage operation = multiSigOperations[operationId];
|
||||
|
||||
operation.operationHash = _operationHash;
|
||||
operation.forSignatures = 0;
|
||||
operation.againstSignatures = 0;
|
||||
operation.executed = false;
|
||||
operation.deadline = block.timestamp + _duration;
|
||||
operation.initiator = msg.sender;
|
||||
|
||||
emit MultiSigOperationCreated(operationId, msg.sender, _operationHash);
|
||||
return operationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Подписать мультиподпись операцию
|
||||
* @param _operationId ID операции
|
||||
* @param _support Поддержка операции
|
||||
*/
|
||||
function signMultiSigOperation(uint256 _operationId, bool _support) external nonReentrant {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
require(block.timestamp < operation.deadline, "Signing ended");
|
||||
require(!operation.executed, "Operation already executed");
|
||||
require(!operation.hasSigned[msg.sender], "Already signed");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to sign");
|
||||
|
||||
uint256 signaturePower = balanceOf(msg.sender);
|
||||
operation.hasSigned[msg.sender] = true;
|
||||
|
||||
if (_support) {
|
||||
operation.forSignatures += signaturePower;
|
||||
} else {
|
||||
operation.againstSignatures += signaturePower;
|
||||
}
|
||||
|
||||
emit MultiSigSigned(_operationId, msg.sender, _support, signaturePower);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизировать мультиподпись из другой цепочки
|
||||
* @param _operationId ID операции
|
||||
* @param _fromChainId ID цепочки откуда синхронизируем
|
||||
* @param _forSignatures Подписи за
|
||||
* @param _againstSignatures Подписи против
|
||||
*/
|
||||
function syncMultiSigFromChain(
|
||||
uint256 _operationId,
|
||||
uint256 _fromChainId,
|
||||
uint256 _forSignatures,
|
||||
uint256 _againstSignatures,
|
||||
bytes memory /* _proof */
|
||||
) external {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
require(supportedChains[_fromChainId], "Chain not supported");
|
||||
require(!operation.chainSignSynced[_fromChainId], "Already synced");
|
||||
|
||||
// Здесь должна быть проверка proof
|
||||
// В реальной реализации нужно проверять доказательство
|
||||
|
||||
operation.forSignatures += _forSignatures;
|
||||
operation.againstSignatures += _againstSignatures;
|
||||
operation.chainSignSynced[_fromChainId] = true;
|
||||
|
||||
emit CrossChainMultiSigSync(_operationId, _fromChainId, currentChainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить результат мультиподписи
|
||||
* @param _operationId ID операции
|
||||
* @return passed Прошла ли операция
|
||||
* @return quorumReached Достигнут ли кворум
|
||||
*/
|
||||
function checkMultiSigResult(uint256 _operationId) public view returns (bool passed, bool quorumReached) {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
|
||||
uint256 totalSignatures = operation.forSignatures + operation.againstSignatures;
|
||||
uint256 quorumRequired = (totalSupply() * quorumPercentage) / 100;
|
||||
|
||||
quorumReached = totalSignatures >= quorumRequired;
|
||||
passed = quorumReached && operation.forSignatures > operation.againstSignatures;
|
||||
|
||||
return (passed, quorumReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Исполнить мультиподпись операцию
|
||||
* @param _operationId ID операции
|
||||
*/
|
||||
function executeMultiSigOperation(uint256 _operationId) external {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
require(!operation.executed, "Operation already executed");
|
||||
require(block.timestamp >= operation.deadline, "Signing not ended");
|
||||
|
||||
(bool passed, bool quorumReached) = checkMultiSigResult(_operationId);
|
||||
require(passed && quorumReached, "Operation not passed");
|
||||
|
||||
operation.executed = true;
|
||||
|
||||
emit MultiSigExecuted(_operationId, operation.operationHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизировать исполнение из другой цепочки
|
||||
* @param _proposalId ID предложения
|
||||
@@ -419,18 +324,37 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
function syncExecutionFromChain(
|
||||
uint256 _proposalId,
|
||||
uint256 _fromChainId,
|
||||
bytes memory /* _proof */
|
||||
bytes memory _proof
|
||||
) external {
|
||||
require(supportedChains[_fromChainId], "Chain not supported");
|
||||
require(!executedProposals[_proposalId], "Already executed");
|
||||
|
||||
// Здесь должна быть проверка proof
|
||||
// В реальной реализации нужно проверять доказательство
|
||||
// Проверяем доказательство исполнения из другой цепочки
|
||||
require(_proof.length > 0, "Proof required for cross-chain execution");
|
||||
|
||||
// Проверяем Merkle proof для cross-chain исполнения
|
||||
bytes32 proofHash = keccak256(abi.encodePacked(_proposalId, _fromChainId, "EXECUTION"));
|
||||
require(!processedProofs[_proposalId][uint256(proofHash)], "Proof already processed");
|
||||
|
||||
// Проверяем, что Merkle root для цепочки установлен
|
||||
bytes32 merkleRoot = chainMerkleRoots[_fromChainId];
|
||||
require(merkleRoot != bytes32(0), "Merkle root not set for chain");
|
||||
|
||||
// Проверяем Merkle proof
|
||||
bytes32[] memory proof = abi.decode(_proof, (bytes32[]));
|
||||
require(MerkleProof.verify(proof, merkleRoot, proofHash), "Invalid Merkle proof");
|
||||
|
||||
// Отмечаем proof как обработанный
|
||||
processedProofs[_proposalId][uint256(proofHash)] = true;
|
||||
|
||||
// Проверяем, что предложение существует и не было исполнено
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
|
||||
executedProposals[_proposalId] = true;
|
||||
|
||||
// Получаем операцию из предложения
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
// Исполняем операцию из предложения
|
||||
if (proposal.id == _proposalId) {
|
||||
_executeOperation(proposal.operation);
|
||||
}
|
||||
@@ -444,9 +368,19 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
* @return isAvailable Доступна ли цепочка
|
||||
*/
|
||||
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
|
||||
// В реальной реализации здесь должна быть проверка подключения
|
||||
// Для примера возвращаем true для поддерживаемых цепочек
|
||||
return supportedChains[_chainId];
|
||||
// Проверяем, поддерживается ли цепочка
|
||||
if (!supportedChains[_chainId]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем, что Merkle root установлен для цепочки
|
||||
// Это означает, что цепочка активна и готова к синхронизации
|
||||
bytes32 merkleRoot = chainMerkleRoots[_chainId];
|
||||
if (merkleRoot == bytes32(0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -491,30 +425,103 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
function syncToChain(uint256 _proposalId, uint256 _chainId) internal {
|
||||
// В реальной реализации здесь будет вызов cross-chain bridge
|
||||
// Для примера просто эмитим событие
|
||||
// Проверяем, что цепочка поддерживается
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
|
||||
// Получаем информацию о предложении
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
|
||||
// Проверяем, что цепочка готова к синхронизации
|
||||
require(checkChainConnection(_chainId), "Chain not ready for sync");
|
||||
|
||||
// Создаем Merkle root для синхронизации
|
||||
bytes32 syncData = keccak256(abi.encodePacked(_proposalId, currentChainId, proposal.operation));
|
||||
|
||||
// Обновляем Merkle root для целевой цепочки
|
||||
chainMerkleRoots[_chainId] = syncData;
|
||||
|
||||
// Эмитим событие для cross-chain bridge
|
||||
emit CrossChainExecutionSync(_proposalId, currentChainId, _chainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить количество поддерживаемых цепочек
|
||||
*/
|
||||
function getSupportedChainCount() public pure returns (uint256) {
|
||||
// В реальной реализации нужно хранить массив поддерживаемых цепочек
|
||||
// Для примера возвращаем 4 (Ethereum, Polygon, BSC, Arbitrum)
|
||||
return 4;
|
||||
function getSupportedChainCount() public view returns (uint256) {
|
||||
return supportedChainIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить ID поддерживаемой цепочки по индексу
|
||||
* @param _index Индекс цепочки
|
||||
*/
|
||||
function getSupportedChainId(uint256 _index) public pure returns (uint256) {
|
||||
if (_index == 0) return 1; // Ethereum
|
||||
if (_index == 1) return 137; // Polygon
|
||||
if (_index == 2) return 56; // BSC
|
||||
if (_index == 3) return 42161; // Arbitrum
|
||||
revert("Invalid chain index");
|
||||
function getSupportedChainId(uint256 _index) public view returns (uint256) {
|
||||
require(_index < supportedChainIds.length, "Invalid chain index");
|
||||
return supportedChainIds[_index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Добавить поддерживаемую цепочку (только для владельцев токенов)
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
function addSupportedChain(uint256 _chainId) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to add chain");
|
||||
require(!supportedChains[_chainId], "Chain already supported");
|
||||
require(_chainId != currentChainId, "Cannot add current chain");
|
||||
|
||||
supportedChains[_chainId] = true;
|
||||
supportedChainIds.push(_chainId);
|
||||
|
||||
emit ChainAdded(_chainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Удалить поддерживаемую цепочку (только для владельцев токенов)
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
function removeSupportedChain(uint256 _chainId) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to remove chain");
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
require(_chainId != currentChainId, "Cannot remove current chain");
|
||||
|
||||
supportedChains[_chainId] = false;
|
||||
|
||||
// Удаляем из массива
|
||||
for (uint256 i = 0; i < supportedChainIds.length; i++) {
|
||||
if (supportedChainIds[i] == _chainId) {
|
||||
supportedChainIds[i] = supportedChainIds[supportedChainIds.length - 1];
|
||||
supportedChainIds.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем Merkle root для цепочки
|
||||
delete chainMerkleRoots[_chainId];
|
||||
|
||||
emit ChainRemoved(_chainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Установить Merkle root для цепочки (только для владельцев токенов)
|
||||
* @param _chainId ID цепочки
|
||||
* @param _merkleRoot Merkle root для цепочки
|
||||
*/
|
||||
function setChainMerkleRoot(uint256 _chainId, bytes32 _merkleRoot) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to set merkle root");
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
|
||||
chainMerkleRoots[_chainId] = _merkleRoot;
|
||||
|
||||
emit ChainMerkleRootSet(_chainId, _merkleRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить Merkle root для цепочки
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
function getChainMerkleRoot(uint256 _chainId) external view returns (bytes32) {
|
||||
return chainMerkleRoots[_chainId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,6 +544,27 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
// Операция сжигания токенов
|
||||
(address from, uint256 amount) = abi.decode(data, (address, uint256));
|
||||
_burn(from, amount);
|
||||
} else if (selector == bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)"))) {
|
||||
// Операция обновления информации DLE
|
||||
(string memory name, string memory symbol, string memory location, string memory coordinates,
|
||||
uint256 jurisdiction, uint256 oktmo, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, uint256, string[], uint256));
|
||||
_updateDLEInfo(name, symbol, location, coordinates, jurisdiction, oktmo, okvedCodes, kpp);
|
||||
} else if (selector == bytes4(keccak256("updateQuorumPercentage(uint256)"))) {
|
||||
// Операция обновления процента кворума
|
||||
(uint256 newQuorumPercentage) = abi.decode(data, (uint256));
|
||||
_updateQuorumPercentage(newQuorumPercentage);
|
||||
} else if (selector == bytes4(keccak256("updateCurrentChainId(uint256)"))) {
|
||||
// Операция обновления текущей цепочки
|
||||
(uint256 newChainId) = abi.decode(data, (uint256));
|
||||
_updateCurrentChainId(newChainId);
|
||||
} else if (selector == bytes4(keccak256("_addModule(bytes32,address)"))) {
|
||||
// Операция добавления модуля
|
||||
(bytes32 moduleId, address moduleAddress) = abi.decode(data, (bytes32, address));
|
||||
_addModule(moduleId, moduleAddress);
|
||||
} else if (selector == bytes4(keccak256("_removeModule(bytes32)"))) {
|
||||
// Операция удаления модуля
|
||||
(bytes32 moduleId) = abi.decode(data, (bytes32));
|
||||
_removeModule(moduleId);
|
||||
} else {
|
||||
// Неизвестная операция
|
||||
revert("Unknown operation");
|
||||
@@ -544,12 +572,156 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Добавить модуль
|
||||
* @dev Обновить информацию DLE
|
||||
* @param _name Новое название
|
||||
* @param _symbol Новый символ
|
||||
* @param _location Новое местонахождение
|
||||
* @param _coordinates Новые координаты
|
||||
* @param _jurisdiction Новая юрисдикция
|
||||
* @param _oktmo Новый ОКТМО
|
||||
* @param _okvedCodes Новые коды ОКВЭД
|
||||
* @param _kpp Новый КПП
|
||||
*/
|
||||
function _updateDLEInfo(
|
||||
string memory _name,
|
||||
string memory _symbol,
|
||||
string memory _location,
|
||||
string memory _coordinates,
|
||||
uint256 _jurisdiction,
|
||||
uint256 _oktmo,
|
||||
string[] memory _okvedCodes,
|
||||
uint256 _kpp
|
||||
) internal {
|
||||
require(bytes(_name).length > 0, "Name cannot be empty");
|
||||
require(bytes(_symbol).length > 0, "Symbol cannot be empty");
|
||||
require(bytes(_location).length > 0, "Location cannot be empty");
|
||||
require(_jurisdiction > 0, "Invalid jurisdiction");
|
||||
require(_oktmo > 0, "Invalid OKTMO");
|
||||
require(_kpp > 0, "Invalid KPP");
|
||||
|
||||
dleInfo.name = _name;
|
||||
dleInfo.symbol = _symbol;
|
||||
dleInfo.location = _location;
|
||||
dleInfo.coordinates = _coordinates;
|
||||
dleInfo.jurisdiction = _jurisdiction;
|
||||
dleInfo.oktmo = _oktmo;
|
||||
dleInfo.okvedCodes = _okvedCodes;
|
||||
dleInfo.kpp = _kpp;
|
||||
|
||||
emit DLEInfoUpdated(_name, _symbol, _location, _coordinates, _jurisdiction, _oktmo, _okvedCodes, _kpp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить процент кворума
|
||||
* @param _newQuorumPercentage Новый процент кворума
|
||||
*/
|
||||
function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal {
|
||||
require(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100, "Invalid quorum percentage");
|
||||
|
||||
uint256 oldQuorumPercentage = quorumPercentage;
|
||||
quorumPercentage = _newQuorumPercentage;
|
||||
|
||||
emit QuorumPercentageUpdated(oldQuorumPercentage, _newQuorumPercentage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить текущую цепочку
|
||||
* @param _newChainId Новый ID цепочки
|
||||
*/
|
||||
function _updateCurrentChainId(uint256 _newChainId) internal {
|
||||
require(supportedChains[_newChainId], "Chain not supported");
|
||||
require(_newChainId != currentChainId, "Same chain ID");
|
||||
|
||||
uint256 oldChainId = currentChainId;
|
||||
currentChainId = _newChainId;
|
||||
|
||||
emit CurrentChainIdUpdated(oldChainId, _newChainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать предложение о добавлении модуля
|
||||
* @param _description Описание предложения
|
||||
* @param _duration Длительность голосования в секундах
|
||||
* @param _moduleId ID модуля
|
||||
* @param _moduleAddress Адрес модуля
|
||||
* @param _chainId ID цепочки для голосования
|
||||
*/
|
||||
function createAddModuleProposal(
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes32 _moduleId,
|
||||
address _moduleAddress,
|
||||
uint256 _chainId
|
||||
) external returns (uint256) {
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
require(checkChainConnection(_chainId), "Chain not available");
|
||||
require(_moduleAddress != address(0), "Zero address");
|
||||
require(!activeModules[_moduleId], "Module already exists");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
|
||||
uint256 proposalId = proposalCounter++;
|
||||
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
proposal.id = proposalId;
|
||||
proposal.description = _description;
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = msg.sender;
|
||||
|
||||
// Кодируем операцию добавления модуля
|
||||
bytes memory operation = abi.encodeWithSelector(
|
||||
bytes4(keccak256("_addModule(bytes32,address)")),
|
||||
_moduleId,
|
||||
_moduleAddress
|
||||
);
|
||||
proposal.operation = operation;
|
||||
|
||||
emit ProposalCreated(proposalId, msg.sender, _description);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать предложение об удалении модуля
|
||||
* @param _description Описание предложения
|
||||
* @param _duration Длительность голосования в секундах
|
||||
* @param _moduleId ID модуля
|
||||
* @param _chainId ID цепочки для голосования
|
||||
*/
|
||||
function createRemoveModuleProposal(
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes32 _moduleId,
|
||||
uint256 _chainId
|
||||
) external returns (uint256) {
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
require(checkChainConnection(_chainId), "Chain not available");
|
||||
require(activeModules[_moduleId], "Module does not exist");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
|
||||
uint256 proposalId = proposalCounter++;
|
||||
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
proposal.id = proposalId;
|
||||
proposal.description = _description;
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = msg.sender;
|
||||
|
||||
// Кодируем операцию удаления модуля
|
||||
bytes memory operation = abi.encodeWithSelector(
|
||||
bytes4(keccak256("_removeModule(bytes32)")),
|
||||
_moduleId
|
||||
);
|
||||
proposal.operation = operation;
|
||||
|
||||
emit ProposalCreated(proposalId, msg.sender, _description);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Добавить модуль (внутренняя функция, вызывается через кворум)
|
||||
* @param _moduleId ID модуля
|
||||
* @param _moduleAddress Адрес модуля
|
||||
*/
|
||||
function addModule(bytes32 _moduleId, address _moduleAddress) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to add module");
|
||||
function _addModule(bytes32 _moduleId, address _moduleAddress) internal {
|
||||
require(_moduleAddress != address(0), "Zero address");
|
||||
require(!activeModules[_moduleId], "Module already exists");
|
||||
|
||||
@@ -560,11 +732,10 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Удалить модуль
|
||||
* @dev Удалить модуль (внутренняя функция, вызывается через кворум)
|
||||
* @param _moduleId ID модуля
|
||||
*/
|
||||
function removeModule(bytes32 _moduleId) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to remove module");
|
||||
function _removeModule(bytes32 _moduleId) internal {
|
||||
require(activeModules[_moduleId], "Module does not exist");
|
||||
|
||||
delete modules[_moduleId];
|
||||
@@ -613,4 +784,190 @@ contract DLE is ERC20, ReentrancyGuard {
|
||||
|
||||
// События для новых функций
|
||||
event SyncCompleted(uint256 proposalId);
|
||||
event DLEDeactivated(address indexed deactivatedBy, uint256 timestamp);
|
||||
event DeactivationProposalCreated(uint256 proposalId, address indexed initiator, string description);
|
||||
event DeactivationProposalVoted(uint256 proposalId, address indexed voter, bool support, uint256 votingPower);
|
||||
event DeactivationProposalExecuted(uint256 proposalId, address indexed executedBy);
|
||||
|
||||
// Структура для предложения деактивации
|
||||
struct DeactivationProposal {
|
||||
uint256 id;
|
||||
string description;
|
||||
uint256 forVotes;
|
||||
uint256 againstVotes;
|
||||
bool executed;
|
||||
uint256 deadline;
|
||||
address initiator;
|
||||
uint256 chainId;
|
||||
mapping(address => bool) hasVoted;
|
||||
}
|
||||
|
||||
// Предложения деактивации
|
||||
mapping(uint256 => DeactivationProposal) public deactivationProposals;
|
||||
uint256 public deactivationProposalCounter;
|
||||
bool public isDeactivated;
|
||||
|
||||
/**
|
||||
* @dev Создать предложение о деактивации DLE
|
||||
* @param _description Описание предложения
|
||||
* @param _duration Длительность голосования в секундах
|
||||
* @param _chainId ID цепочки для деактивации
|
||||
*/
|
||||
function createDeactivationProposal(
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
uint256 _chainId
|
||||
) external returns (uint256) {
|
||||
require(!isDeactivated, "DLE already deactivated");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create deactivation proposal");
|
||||
require(_duration > 0, "Duration must be positive");
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
|
||||
uint256 proposalId = deactivationProposalCounter++;
|
||||
DeactivationProposal storage proposal = deactivationProposals[proposalId];
|
||||
|
||||
proposal.id = proposalId;
|
||||
proposal.description = _description;
|
||||
proposal.forVotes = 0;
|
||||
proposal.againstVotes = 0;
|
||||
proposal.executed = false;
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = msg.sender;
|
||||
proposal.chainId = _chainId;
|
||||
|
||||
emit DeactivationProposalCreated(proposalId, msg.sender, _description);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Голосовать за предложение деактивации
|
||||
* @param _proposalId ID предложения
|
||||
* @param _support Поддержка предложения
|
||||
*/
|
||||
function voteDeactivation(uint256 _proposalId, bool _support) external nonReentrant {
|
||||
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
|
||||
require(block.timestamp < proposal.deadline, "Voting ended");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(!proposal.hasVoted[msg.sender], "Already voted");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to vote");
|
||||
|
||||
uint256 votingPower = balanceOf(msg.sender);
|
||||
|
||||
if (_support) {
|
||||
proposal.forVotes += votingPower;
|
||||
} else {
|
||||
proposal.againstVotes += votingPower;
|
||||
}
|
||||
|
||||
proposal.hasVoted[msg.sender] = true;
|
||||
|
||||
emit DeactivationProposalVoted(_proposalId, msg.sender, _support, votingPower);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить результат предложения деактивации
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function checkDeactivationProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
|
||||
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
|
||||
|
||||
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
|
||||
uint256 totalSupply = totalSupply();
|
||||
|
||||
quorumReached = totalVotes >= (totalSupply * quorumPercentage) / 100;
|
||||
passed = quorumReached && proposal.forVotes > proposal.againstVotes;
|
||||
|
||||
return (passed, quorumReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Исполнить предложение деактивации
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function executeDeactivationProposal(uint256 _proposalId) external {
|
||||
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp >= proposal.deadline, "Voting not ended");
|
||||
|
||||
(bool passed, bool quorumReached) = checkDeactivationProposalResult(_proposalId);
|
||||
require(quorumReached, "Quorum not reached");
|
||||
require(passed, "Proposal not passed");
|
||||
|
||||
proposal.executed = true;
|
||||
isDeactivated = true;
|
||||
dleInfo.isActive = false;
|
||||
|
||||
emit DeactivationProposalExecuted(_proposalId, msg.sender);
|
||||
emit DLEDeactivated(msg.sender, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Деактивировать DLE напрямую (только при достижении кворума)
|
||||
* Может быть вызвана только если есть активное предложение деактивации с достигнутым кворумом
|
||||
*/
|
||||
function deactivate() external {
|
||||
require(!isDeactivated, "DLE already deactivated");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to deactivate DLE");
|
||||
|
||||
// Проверяем, есть ли активное предложение деактивации с достигнутым кворумом
|
||||
bool hasValidDeactivationProposal = false;
|
||||
|
||||
for (uint256 i = 0; i < deactivationProposalCounter; i++) {
|
||||
DeactivationProposal storage proposal = deactivationProposals[i];
|
||||
if (!proposal.executed && block.timestamp >= proposal.deadline) {
|
||||
(bool passed, bool quorumReached) = checkDeactivationProposalResult(i);
|
||||
if (quorumReached && passed) {
|
||||
hasValidDeactivationProposal = true;
|
||||
proposal.executed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require(hasValidDeactivationProposal, "No valid deactivation proposal with quorum");
|
||||
|
||||
isDeactivated = true;
|
||||
dleInfo.isActive = false;
|
||||
|
||||
emit DLEDeactivated(msg.sender, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить, деактивирован ли DLE
|
||||
*/
|
||||
function isActive() external view returns (bool) {
|
||||
return !isDeactivated && dleInfo.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить информацию о предложении деактивации
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function getDeactivationProposal(uint256 _proposalId) external view returns (
|
||||
uint256 id,
|
||||
string memory description,
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
bool executed,
|
||||
uint256 deadline,
|
||||
address initiator,
|
||||
uint256 chainId
|
||||
) {
|
||||
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
|
||||
|
||||
return (
|
||||
proposal.id,
|
||||
proposal.description,
|
||||
proposal.forVotes,
|
||||
proposal.againstVotes,
|
||||
proposal.executed,
|
||||
proposal.deadline,
|
||||
proposal.initiator,
|
||||
proposal.chainId
|
||||
);
|
||||
}
|
||||
}
|
||||
9
backend/db/migrations/049_add_message_id_encrypted.sql
Normal file
9
backend/db/migrations/049_add_message_id_encrypted.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Миграция: добавление зашифрованной колонки message_id_encrypted в таблицу messages
|
||||
-- Для хранения Message-ID email писем для дедупликации
|
||||
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_id_encrypted TEXT NULL;
|
||||
|
||||
-- Создаем индекс для быстрого поиска по message_id (если нужно будет в будущем)
|
||||
-- CREATE INDEX IF NOT EXISTS idx_messages_message_id_encrypted ON messages(message_id_encrypted);
|
||||
|
||||
COMMENT ON COLUMN messages.message_id_encrypted IS 'Зашифрованный Message-ID email письма для дедупликации';
|
||||
@@ -49,7 +49,6 @@ router.post('/task', requireAuth, async (req, res) => {
|
||||
|
||||
const taskData = {
|
||||
message,
|
||||
language: language || 'auto',
|
||||
history: history || null,
|
||||
systemPrompt: systemPrompt || '',
|
||||
rules: rules || null,
|
||||
|
||||
@@ -77,6 +77,7 @@ router.post('/read-dle-info', async (req, res) => {
|
||||
const blockchainData = {
|
||||
name: dleInfo.name,
|
||||
symbol: dleInfo.symbol,
|
||||
dleAddress: dleAddress, // Добавляем адрес контракта
|
||||
location: dleInfo.location,
|
||||
coordinates: dleInfo.coordinates,
|
||||
jurisdiction: Number(dleInfo.jurisdiction),
|
||||
@@ -400,6 +401,309 @@ router.post('/get-proposal-info', async (req, res) => {
|
||||
|
||||
|
||||
|
||||
// Проверка возможности деактивации DLE
|
||||
router.post('/deactivate-dle', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress, userAddress } = req.body;
|
||||
|
||||
if (!dleAddress || !userAddress) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Адрес DLE и адрес пользователя обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`);
|
||||
|
||||
// Получаем RPC URL для Sepolia
|
||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||
if (!rpcUrl) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'RPC URL для Sepolia не найден'
|
||||
});
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
// ABI для проверки деактивации DLE
|
||||
const dleAbi = [
|
||||
"function isActive() external view returns (bool)",
|
||||
"function balanceOf(address) external view returns (uint256)"
|
||||
];
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||
|
||||
// Проверяем, что пользователь имеет токены
|
||||
const balance = await dle.balanceOf(userAddress);
|
||||
if (balance <= 0) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Для деактивации DLE необходимо иметь токены'
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем текущий статус
|
||||
const isActive = await dle.isActive();
|
||||
if (!isActive) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'DLE уже деактивирован'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] DLE ${dleAddress} может быть деактивирован пользователем ${userAddress}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
dleAddress: dleAddress,
|
||||
canDeactivate: true,
|
||||
message: 'DLE может быть деактивирован при наличии валидного предложения с кворумом.'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Blockchain] Ошибка при проверке возможности деактивации DLE:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при проверке возможности деактивации DLE: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Проверить результат предложения деактивации
|
||||
router.post('/check-deactivation-proposal-result', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress, proposalId } = req.body;
|
||||
|
||||
if (!dleAddress || proposalId === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Адрес DLE и ID предложения обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] Проверка результата предложения деактивации: ${proposalId} для DLE: ${dleAddress}`);
|
||||
|
||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||
if (!rpcUrl) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'RPC URL для Sepolia не найден'
|
||||
});
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
const dleAbi = [
|
||||
"function checkDeactivationProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached)"
|
||||
];
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||
|
||||
const [passed, quorumReached] = await dle.checkDeactivationProposalResult(proposalId);
|
||||
|
||||
const result = {
|
||||
proposalId: proposalId,
|
||||
passed: passed,
|
||||
quorumReached: quorumReached,
|
||||
canExecute: passed && quorumReached
|
||||
};
|
||||
|
||||
console.log(`[Blockchain] Результат предложения деактивации:`, result);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Blockchain] Ошибка при проверке результата предложения деактивации:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при проверке результата предложения деактивации: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Загрузить предложения деактивации
|
||||
router.post('/load-deactivation-proposals', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress } = req.body;
|
||||
|
||||
if (!dleAddress) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Адрес DLE обязателен'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] Загрузка предложений деактивации для DLE: ${dleAddress}`);
|
||||
|
||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||
if (!rpcUrl) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'RPC URL для Sepolia не найден'
|
||||
});
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
const dleAbi = [
|
||||
"function deactivationProposalCounter() external view returns (uint256)",
|
||||
"function getDeactivationProposal(uint256 _proposalId) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, uint256 deadline, address initiator, uint256 chainId)"
|
||||
];
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||
|
||||
const proposalCounter = await dle.deactivationProposalCounter();
|
||||
const proposals = [];
|
||||
|
||||
for (let i = 0; i < proposalCounter; i++) {
|
||||
try {
|
||||
const proposal = await dle.getDeactivationProposal(i);
|
||||
proposals.push({
|
||||
id: Number(proposal.id),
|
||||
description: proposal.description,
|
||||
forVotes: ethers.formatUnits(proposal.forVotes, 18),
|
||||
againstVotes: ethers.formatUnits(proposal.againstVotes, 18),
|
||||
executed: proposal.executed,
|
||||
deadline: Number(proposal.deadline),
|
||||
initiator: proposal.initiator,
|
||||
chainId: Number(proposal.chainId),
|
||||
isExpired: Date.now() / 1000 > Number(proposal.deadline)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[Blockchain] Ошибка при загрузке предложения ${i}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] Загружено ${proposals.length} предложений деактивации`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
proposals: proposals
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Blockchain] Ошибка при загрузке предложений деактивации:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при загрузке предложений деактивации: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Создать предложение о добавлении модуля
|
||||
router.post('/create-add-module-proposal', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress, description, duration, moduleId, moduleAddress, chainId } = req.body;
|
||||
|
||||
if (!dleAddress || !description || !duration || !moduleId || !moduleAddress || !chainId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Все поля обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] Создание предложения о добавлении модуля: ${moduleId} для DLE: ${dleAddress}`);
|
||||
|
||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||
if (!rpcUrl) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'RPC URL для Sepolia не найден'
|
||||
});
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
const dleAbi = [
|
||||
"function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
|
||||
];
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||
|
||||
// Создаем предложение
|
||||
const tx = await dle.createAddModuleProposal(description, duration, moduleId, moduleAddress, chainId);
|
||||
const receipt = await tx.wait();
|
||||
|
||||
console.log(`[Blockchain] Предложение о добавлении модуля создано:`, receipt);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
proposalId: receipt.logs[0].args.proposalId,
|
||||
transactionHash: receipt.hash
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Blockchain] Ошибка при создании предложения о добавлении модуля:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при создании предложения о добавлении модуля: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Создать предложение об удалении модуля
|
||||
router.post('/create-remove-module-proposal', async (req, res) => {
|
||||
try {
|
||||
const { dleAddress, description, duration, moduleId, chainId } = req.body;
|
||||
|
||||
if (!dleAddress || !description || !duration || !moduleId || !chainId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Все поля обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Blockchain] Создание предложения об удалении модуля: ${moduleId} для DLE: ${dleAddress}`);
|
||||
|
||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||
if (!rpcUrl) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'RPC URL для Sepolia не найден'
|
||||
});
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
const dleAbi = [
|
||||
"function createRemoveModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) external returns (uint256)"
|
||||
];
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||
|
||||
// Создаем предложение
|
||||
const tx = await dle.createRemoveModuleProposal(description, duration, moduleId, chainId);
|
||||
const receipt = await tx.wait();
|
||||
|
||||
console.log(`[Blockchain] Предложение об удалении модуля создано:`, receipt);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
proposalId: receipt.logs[0].args.proposalId,
|
||||
transactionHash: receipt.hash
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Blockchain] Ошибка при создании предложения об удалении модуля:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при создании предложения об удалении модуля: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Импортируем WebSocket функции из wsHub
|
||||
const { broadcastProposalCreated, broadcastProposalVoted, broadcastProposalExecuted } = require('../wsHub');
|
||||
|
||||
|
||||
@@ -171,12 +171,9 @@ async function processGuestMessages(userId, guestId) {
|
||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
// Язык guestMessage.language или auto
|
||||
const detectedLanguage = guestMessage.language === 'auto' ? aiAssistant.detectLanguage(guestMessage.content) : guestMessage.language;
|
||||
logger.info('Getting AI response for guest message:', guestMessage.content);
|
||||
const aiResponseContent = await aiAssistant.getResponse(
|
||||
guestMessage.content,
|
||||
detectedLanguage,
|
||||
history,
|
||||
aiSettings ? aiSettings.system_prompt : '',
|
||||
rules ? rules.rules : null
|
||||
@@ -310,7 +307,7 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||||
[
|
||||
guestId,
|
||||
messageContent, // Текст сообщения или NULL
|
||||
language || 'auto',
|
||||
'ru', // Устанавливаем русский язык по умолчанию
|
||||
attachmentFilename,
|
||||
attachmentMimetype,
|
||||
attachmentSize,
|
||||
@@ -511,7 +508,14 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
if (aiSettings && aiSettings.rules_id) {
|
||||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||
}
|
||||
// --- RAG автоответ ---
|
||||
// --- RAG автоответ с поддержкой беседы ---
|
||||
// Пример работы:
|
||||
// 1. Пользователь: "Как подключить кошелек?"
|
||||
// RAG: находит точный ответ → возвращает его
|
||||
// 2. Пользователь: "А какие документы нужны?"
|
||||
// RAG: анализирует контекст предыдущего ответа → ищет информацию о документах
|
||||
// 3. Пользователь: "Сколько это займет времени?"
|
||||
// RAG: использует полный контекст беседы → дает уточненный ответ
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
@@ -520,11 +524,29 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
}
|
||||
let ragResult = null;
|
||||
if (ragTableId) {
|
||||
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
|
||||
const threshold = 0.3;
|
||||
logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
|
||||
const { ragAnswerWithConversation, generateLLMResponse } = require('../services/ragService');
|
||||
const threshold = 200; // Увеличиваем threshold для более широкого поиска
|
||||
|
||||
// Получаем историю беседы
|
||||
const historyResult = await db.getQuery()(
|
||||
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
||||
[conversationId, userMessage.id, encryptionKey]
|
||||
);
|
||||
const history = historyResult.rows.reverse().map(msg => ({
|
||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
|
||||
logger.info(`[RAG] Запуск поиска по RAG с беседой: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}, historyLength=${history.length}`);
|
||||
const ragResult = await ragAnswerWithConversation({
|
||||
tableId: ragTableId,
|
||||
userQuestion: messageContent,
|
||||
threshold,
|
||||
history,
|
||||
conversationId
|
||||
});
|
||||
logger.info(`[RAG] Результат поиска по RAG:`, ragResult);
|
||||
logger.info(`[RAG] Score type: ${typeof ragResult.score}, value: ${ragResult.score}, threshold: ${threshold}, isFollowUp: ${ragResult.isFollowUp}`);
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= threshold) {
|
||||
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
||||
// Прямой ответ из RAG
|
||||
@@ -542,15 +564,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
broadcastChatMessage(aiMessage);
|
||||
} else if (ragResult) {
|
||||
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
||||
// Генерация через LLM с подстановкой значений из RAG
|
||||
const historyResult = await db.getQuery()(
|
||||
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
||||
[conversationId, userMessage.id, encryptionKey]
|
||||
);
|
||||
const history = historyResult.rows.reverse().map(msg => ({
|
||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
// Генерация через LLM с подстановкой значений из RAG и историей беседы
|
||||
const llmResponse = await generateLLMResponse({
|
||||
userQuestion: messageContent,
|
||||
context: ragResult.context,
|
||||
@@ -558,9 +572,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
clarifyingAnswer: ragResult.clarifyingAnswer,
|
||||
objectionAnswer: ragResult.objectionAnswer,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||||
history: ragResult.conversationContext ? ragResult.conversationContext.conversationHistory : history,
|
||||
model: aiSettings ? aiSettings.model : undefined
|
||||
});
|
||||
if (llmResponse) {
|
||||
aiMessage = await encryptedDb.saveData('messages', {
|
||||
@@ -824,7 +837,6 @@ router.post('/message-queued', requireAuth, upload.array('attachments'), async (
|
||||
// Добавляем задачу в очередь
|
||||
const taskData = {
|
||||
message: messageContent,
|
||||
language: language || 'auto',
|
||||
history: history,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
rules: rules,
|
||||
@@ -927,7 +939,10 @@ router.get('/history', requireAuth, async (req, res) => {
|
||||
whereConditions.conversation_id = conversationId;
|
||||
}
|
||||
|
||||
const messages = await encryptedDb.getData('messages', whereConditions, limit, 'created_at ASC', offset);
|
||||
// Изменяем логику: загружаем ПОСЛЕДНИЕ сообщения, а не с offset
|
||||
const messages = await encryptedDb.getData('messages', whereConditions, limit, 'created_at DESC', 0);
|
||||
// Переворачиваем массив для правильного порядка
|
||||
messages.reverse();
|
||||
|
||||
// Обрабатываем результаты для фронтенда
|
||||
const formattedMessages = messages.map(msg => {
|
||||
@@ -1057,7 +1072,6 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
|
||||
logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
|
||||
}
|
||||
const { generateLLMResponse } = require('../services/ragService');
|
||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
||||
const aiResponseContent = await generateLLMResponse({
|
||||
userQuestion: promptText,
|
||||
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||
@@ -1065,7 +1079,6 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru',
|
||||
rules: rules ? rules.rules : null
|
||||
});
|
||||
res.json({ success: true, aiMessage: aiResponseContent });
|
||||
|
||||
@@ -27,6 +27,7 @@ const telegramBot = require('../services/telegramBot');
|
||||
const EmailBotService = require('../services/emailBot');
|
||||
const emailBotService = new EmailBotService();
|
||||
const dbSettingsService = require('../services/dbSettingsService');
|
||||
const { broadcastAuthTokenAdded, broadcastAuthTokenDeleted, broadcastAuthTokenUpdated } = require('../wsHub');
|
||||
|
||||
// Логируем версию ethers для отладки
|
||||
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
||||
@@ -163,6 +164,16 @@ router.post('/auth-tokens', requireAdmin, async (req, res, next) => {
|
||||
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
||||
}
|
||||
await authTokenService.saveAllAuthTokens(authTokens);
|
||||
|
||||
// После сохранения токенов перепроверяем баланс ВСЕХ авторизованных пользователей
|
||||
const authService = require('../services/auth-service');
|
||||
try {
|
||||
await authService.recheckAllUsersAdminStatus();
|
||||
logger.info('Балансы всех пользователей перепроверены после сохранения токенов');
|
||||
} catch (balanceError) {
|
||||
logger.error(`Ошибка при перепроверке балансов всех пользователей: ${balanceError.message}`);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при сохранении токенов аутентификации:', error);
|
||||
@@ -178,6 +189,24 @@ router.post('/auth-token', requireAdmin, async (req, res, next) => {
|
||||
return res.status(400).json({ success: false, error: 'name, address и network обязательны' });
|
||||
}
|
||||
await authTokenService.upsertAuthToken({ name, address, network, minBalance });
|
||||
|
||||
// Отправляем WebSocket уведомление о добавлении токена
|
||||
try {
|
||||
broadcastAuthTokenAdded({ name, address, network, minBalance });
|
||||
logger.info('WebSocket уведомление о добавлении токена отправлено');
|
||||
} catch (wsError) {
|
||||
logger.error(`Ошибка при отправке WebSocket уведомления: ${wsError.message}`);
|
||||
}
|
||||
|
||||
// После добавления токена перепроверяем баланс ВСЕХ авторизованных пользователей
|
||||
const authService = require('../services/auth-service');
|
||||
try {
|
||||
await authService.recheckAllUsersAdminStatus();
|
||||
logger.info('Балансы всех пользователей перепроверены после добавления токена');
|
||||
} catch (balanceError) {
|
||||
logger.error(`Ошибка при перепроверке балансов всех пользователей: ${balanceError.message}`);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Токен аутентификации сохранён' });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при сохранении токена аутентификации:', error);
|
||||
@@ -190,6 +219,24 @@ router.delete('/auth-token/:address/:network', requireAdmin, async (req, res, ne
|
||||
try {
|
||||
const { address, network } = req.params;
|
||||
await authTokenService.deleteAuthToken(address, network);
|
||||
|
||||
// Отправляем WebSocket уведомление об удалении токена
|
||||
try {
|
||||
broadcastAuthTokenDeleted({ address, network });
|
||||
logger.info('WebSocket уведомление об удалении токена отправлено');
|
||||
} catch (wsError) {
|
||||
logger.error(`Ошибка при отправке WebSocket уведомления: ${wsError.message}`);
|
||||
}
|
||||
|
||||
// После удаления токена перепроверяем баланс ВСЕХ авторизованных пользователей
|
||||
const authService = require('../services/auth-service');
|
||||
try {
|
||||
await authService.recheckAllUsersAdminStatus();
|
||||
logger.info('Балансы всех пользователей перепроверены после удаления токена');
|
||||
} catch (balanceError) {
|
||||
logger.error(`Ошибка при перепроверке балансов всех пользователей: ${balanceError.message}`);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Токен аутентификации удалён' });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при удалении токена аутентификации:', error);
|
||||
|
||||
@@ -70,43 +70,50 @@ class AIAssistant {
|
||||
}
|
||||
|
||||
// Создание экземпляра ChatOllama с нужными параметрами
|
||||
createChat(language = 'ru', customSystemPrompt = '') {
|
||||
createChat(customSystemPrompt = '') {
|
||||
// Используем кастомный системный промпт, если он передан, иначе используем дефолтный
|
||||
let systemPrompt = customSystemPrompt;
|
||||
if (!systemPrompt) {
|
||||
systemPrompt = language === 'ru'
|
||||
? 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.'
|
||||
: 'You are a helpful assistant. Respond in English briefly and to the point.';
|
||||
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.';
|
||||
}
|
||||
|
||||
return new ChatOllama({
|
||||
baseUrl: this.baseUrl,
|
||||
model: this.defaultModel,
|
||||
system: systemPrompt,
|
||||
temperature: 0.3, // Уменьшаем для более предсказуемых ответов
|
||||
maxTokens: 100, // Еще больше уменьшаем для быстрого ответа
|
||||
timeout: 60000, // Увеличиваем таймаут до 60 секунд
|
||||
temperature: 0.7, // Восстанавливаем для более творческих ответов
|
||||
maxTokens: 2048, // Восстанавливаем для полных ответов
|
||||
timeout: 300000, // 5 минут для качественной обработки
|
||||
numCtx: 4096, // Увеличиваем контекст для лучшего понимания
|
||||
numGpu: 1, // Используем GPU
|
||||
numThread: 8, // Оптимальное количество потоков
|
||||
repeatPenalty: 1.1, // Штраф за повторения
|
||||
topK: 40, // Разнообразие ответов
|
||||
topP: 0.9, // Ядерная выборка
|
||||
tfsZ: 1, // Tail free sampling
|
||||
mirostat: 2, // Mirostat 2.0 для контроля качества
|
||||
mirostatTau: 5, // Целевая перплексия
|
||||
mirostatEta: 0.1, // Скорость адаптации
|
||||
grammar: '', // Грамматика (если нужна)
|
||||
seed: -1, // Случайный сид
|
||||
numPredict: -1, // Неограниченная длина
|
||||
stop: [], // Стоп-слова
|
||||
stream: false, // Без стриминга для стабильности
|
||||
options: {
|
||||
num_ctx: 512, // Еще больше уменьшаем контекст для экономии памяти
|
||||
num_thread: 12, // Увеличиваем количество потоков еще больше
|
||||
num_gpu: 1,
|
||||
num_gqa: 8,
|
||||
rope_freq_base: 1000000,
|
||||
rope_freq_scale: 0.5,
|
||||
repeat_penalty: 1.1, // Добавляем штраф за повторения
|
||||
top_k: 20, // Еще больше ограничиваем выбор токенов
|
||||
top_p: 0.8, // Уменьшаем nucleus sampling
|
||||
temperature: 0.1, // Еще больше уменьшаем для более предсказуемых ответов
|
||||
numCtx: 4096,
|
||||
numGpu: 1,
|
||||
numThread: 8,
|
||||
repeatPenalty: 1.1,
|
||||
topK: 40,
|
||||
topP: 0.9,
|
||||
tfsZ: 1,
|
||||
mirostat: 2,
|
||||
mirostatTau: 5,
|
||||
mirostatEta: 0.1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Определение языка сообщения
|
||||
detectLanguage(message) {
|
||||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
||||
return cyrillicPattern.test(message) ? 'ru' : 'en';
|
||||
}
|
||||
|
||||
// Определение приоритета запроса
|
||||
getRequestPriority(message, history, rules) {
|
||||
let priority = 0;
|
||||
@@ -117,7 +124,7 @@ class AIAssistant {
|
||||
}
|
||||
|
||||
// Приоритет по типу запроса
|
||||
const urgentKeywords = ['срочно', 'urgent', 'важно', 'important', 'помоги', 'help'];
|
||||
const urgentKeywords = ['срочно', 'важно', 'помоги'];
|
||||
if (urgentKeywords.some(keyword => message.toLowerCase().includes(keyword))) {
|
||||
priority += 20;
|
||||
}
|
||||
@@ -140,9 +147,9 @@ class AIAssistant {
|
||||
}
|
||||
|
||||
// Основной метод для получения ответа
|
||||
async getResponse(message, language = 'auto', history = null, systemPrompt = '', rules = null) {
|
||||
async getResponse(message, history = null, systemPrompt = '', rules = null) {
|
||||
try {
|
||||
// console.log('getResponse called with:', { message, language, history, systemPrompt, rules });
|
||||
// console.log('getResponse called with:', { message, history, systemPrompt, rules });
|
||||
|
||||
// Очищаем старый кэш
|
||||
this.cleanupCache();
|
||||
@@ -171,7 +178,6 @@ class AIAssistant {
|
||||
// Добавляем запрос в очередь
|
||||
const requestId = await aiQueue.addRequest({
|
||||
message,
|
||||
language,
|
||||
history,
|
||||
systemPrompt,
|
||||
rules
|
||||
@@ -181,7 +187,7 @@ class AIAssistant {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timeout - очередь перегружена'));
|
||||
}, 60000); // 60 секунд таймаут для очереди
|
||||
}, 180000); // 180 секунд таймаут для очереди (увеличено с 60)
|
||||
|
||||
const onCompleted = (item) => {
|
||||
if (item.id === requestId) {
|
||||
@@ -204,62 +210,6 @@ class AIAssistant {
|
||||
aiQueue.on('completed', onCompleted);
|
||||
aiQueue.on('failed', onFailed);
|
||||
});
|
||||
|
||||
// Определяем язык, если не указан явно
|
||||
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
|
||||
// console.log('Detected language:', detectedLanguage);
|
||||
|
||||
// Формируем system prompt с учётом правил
|
||||
let fullSystemPrompt = systemPrompt || '';
|
||||
if (rules && typeof rules === 'object') {
|
||||
fullSystemPrompt += '\n' + JSON.stringify(rules, null, 2);
|
||||
}
|
||||
|
||||
// Формируем массив сообщений для Qwen2.5/OpenAI API
|
||||
const messages = [];
|
||||
if (fullSystemPrompt) {
|
||||
messages.push({ role: 'system', content: fullSystemPrompt });
|
||||
}
|
||||
if (Array.isArray(history) && history.length > 0) {
|
||||
for (const msg of history) {
|
||||
if (msg.role && msg.content) {
|
||||
messages.push({ role: msg.role, content: msg.content });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Добавляем текущее сообщение пользователя
|
||||
messages.push({ role: 'user', content: message });
|
||||
|
||||
let response = null;
|
||||
|
||||
// Пробуем прямой API запрос (OpenAI-совместимый endpoint)
|
||||
try {
|
||||
// console.log('Trying direct API request...');
|
||||
response = await this.fallbackRequestOpenAI(messages, detectedLanguage, fullSystemPrompt);
|
||||
// console.log('Direct API response received:', response);
|
||||
} catch (error) {
|
||||
// console.error('Error in direct API request:', error);
|
||||
|
||||
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
|
||||
const chat = this.createChat(detectedLanguage, fullSystemPrompt);
|
||||
try {
|
||||
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
|
||||
// console.log('Sending request to ChatOllama...');
|
||||
const chatResponse = await chat.invoke(prompt);
|
||||
// console.log('ChatOllama response:', chatResponse);
|
||||
response = chatResponse.content;
|
||||
} catch (chatError) {
|
||||
// console.error('Error using ChatOllama:', chatError);
|
||||
throw chatError;
|
||||
}
|
||||
}
|
||||
|
||||
// Кэшируем ответ
|
||||
if (response) {
|
||||
aiCache.set(cacheKey, response);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// console.error('Error in getResponse:', error);
|
||||
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
||||
@@ -267,9 +217,9 @@ class AIAssistant {
|
||||
}
|
||||
|
||||
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
|
||||
async fallbackRequestOpenAI(messages, language, systemPrompt = '') {
|
||||
async fallbackRequestOpenAI(messages, systemPrompt = '') {
|
||||
try {
|
||||
// console.log('Using fallbackRequestOpenAI with:', { messages, language, systemPrompt });
|
||||
// console.log('Using fallbackRequestOpenAI with:', { messages, systemPrompt });
|
||||
const model = this.defaultModel;
|
||||
|
||||
// Создаем AbortController для таймаута
|
||||
@@ -284,23 +234,25 @@ class AIAssistant {
|
||||
messages,
|
||||
stream: false,
|
||||
options: {
|
||||
temperature: 0.3,
|
||||
num_predict: 150, // Уменьшаем максимальную длину ответа для ускорения
|
||||
num_ctx: 512, // Уменьшаем контекст для экономии памяти и ускорения
|
||||
num_thread: 12, // Увеличиваем количество потоков для ускорения
|
||||
temperature: 0.7,
|
||||
num_predict: 2048, // Восстанавливаем для полных ответов
|
||||
num_ctx: 4096, // Восстанавливаем контекст для лучшего понимания
|
||||
num_thread: 8, // Оптимальное количество потоков
|
||||
num_gpu: 1, // Используем GPU если доступен
|
||||
num_gqa: 8, // Оптимизация для qwen2.5
|
||||
rope_freq_base: 1000000, // Оптимизация для qwen2.5
|
||||
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
|
||||
repeat_penalty: 1.1, // Добавляем штраф за повторения
|
||||
top_k: 20, // Уменьшаем выбор токенов для ускорения
|
||||
top_p: 0.8, // Уменьшаем nucleus sampling для ускорения
|
||||
mirostat: 2, // Используем mirostat для стабильности
|
||||
mirostat_tau: 5.0, // Настройка mirostat
|
||||
mirostat_eta: 0.1, // Настройка mirostat
|
||||
},
|
||||
}),
|
||||
signal: controller.signal,
|
||||
repeat_penalty: 1.1, // Восстанавливаем штраф за повторения
|
||||
top_k: 40, // Восстанавливаем разнообразие ответов
|
||||
top_p: 0.9, // Восстанавливаем nucleus sampling
|
||||
tfs_z: 1, // Tail free sampling
|
||||
mirostat: 2, // Mirostat 2.0 для контроля качества
|
||||
mirostat_tau: 5, // Целевая перплексия
|
||||
mirostat_eta: 0.1, // Скорость адаптации
|
||||
seed: -1, // Случайный сид
|
||||
stop: [] // Стоп-слова
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -10,7 +10,7 @@ class AIQueue extends EventEmitter {
|
||||
super();
|
||||
this.queue = [];
|
||||
this.processing = false;
|
||||
this.maxConcurrent = 2; // Максимум 2 запроса одновременно
|
||||
this.maxConcurrent = 1; // Максимум 1 запрос одновременно (последовательная обработка)
|
||||
this.activeRequests = 0;
|
||||
this.stats = {
|
||||
total: 0,
|
||||
@@ -51,6 +51,7 @@ class AIQueue extends EventEmitter {
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
logger.info(`[AIQueue] Начинаем обработку очереди. Запросов в очереди: ${this.queue.length}`);
|
||||
|
||||
while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
|
||||
const item = this.queue.shift();
|
||||
@@ -58,6 +59,7 @@ class AIQueue extends EventEmitter {
|
||||
|
||||
this.activeRequests++;
|
||||
item.status = 'processing';
|
||||
logger.info(`[AIQueue] Обрабатываем запрос ${item.id} (приоритет: ${item.priority})`);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
@@ -71,7 +73,7 @@ class AIQueue extends EventEmitter {
|
||||
this.stats.completed++;
|
||||
this.updateAvgResponseTime(responseTime);
|
||||
|
||||
logger.info(`[AIQueue] Request ${item.id} completed in ${responseTime}ms`);
|
||||
logger.info(`[AIQueue] Запрос ${item.id} завершен за ${responseTime}ms`);
|
||||
|
||||
// Эмитим событие о завершении
|
||||
this.emit('completed', item);
|
||||
@@ -81,7 +83,7 @@ class AIQueue extends EventEmitter {
|
||||
item.error = error.message;
|
||||
|
||||
this.stats.failed++;
|
||||
logger.error(`[AIQueue] Request ${item.id} failed:`, error.message);
|
||||
logger.error(`[AIQueue] Запрос ${item.id} завершился с ошибкой:`, error.message);
|
||||
|
||||
// Эмитим событие об ошибке
|
||||
this.emit('failed', item);
|
||||
@@ -91,6 +93,7 @@ class AIQueue extends EventEmitter {
|
||||
}
|
||||
|
||||
this.processing = false;
|
||||
logger.info(`[AIQueue] Обработка очереди завершена. Осталось запросов: ${this.queue.length}`);
|
||||
|
||||
// Если в очереди еще есть запросы, продолжаем обработку
|
||||
if (this.queue.length > 0) {
|
||||
@@ -118,7 +121,7 @@ class AIQueue extends EventEmitter {
|
||||
messages.push({ role: 'user', content: request.message });
|
||||
|
||||
// Прямой вызов API без очереди
|
||||
return await aiAssistant.fallbackRequestOpenAI(messages, request.language, request.systemPrompt);
|
||||
return await aiAssistant.fallbackRequestOpenAI(messages, request.systemPrompt);
|
||||
}
|
||||
|
||||
// Обновление средней скорости ответа
|
||||
|
||||
@@ -50,6 +50,7 @@ async function getSettings() {
|
||||
);
|
||||
supportEmail = em.rows[0] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
...setting,
|
||||
telegramBot,
|
||||
@@ -58,12 +59,12 @@ async function getSettings() {
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
|
||||
async function upsertSettings({ system_prompt, selected_rag_tables, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
|
||||
const data = {
|
||||
id: 1,
|
||||
system_prompt,
|
||||
selected_rag_tables,
|
||||
languages,
|
||||
languages: ['ru'], // Устанавливаем русский язык по умолчанию
|
||||
model,
|
||||
embedding_model,
|
||||
rules,
|
||||
|
||||
@@ -519,6 +519,20 @@ class AuthService {
|
||||
} else {
|
||||
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
const userResult = await db.getQuery()(
|
||||
`
|
||||
SELECT u.id, u.role FROM users u
|
||||
@@ -544,6 +558,76 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перепроверяет админский статус ВСЕХ пользователей с кошельками
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async recheckAllUsersAdminStatus() {
|
||||
logger.info('Starting recheck of admin status for all users with wallets');
|
||||
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Получаем всех пользователей с кошельками
|
||||
const usersResult = await db.getQuery()(
|
||||
`
|
||||
SELECT DISTINCT u.id, u.role, decrypt_text(ui.provider_id_encrypted, $1) as address
|
||||
FROM users u
|
||||
JOIN user_identities ui ON u.id = ui.user_id
|
||||
WHERE ui.provider_encrypted = encrypt_text('wallet', $1)
|
||||
`,
|
||||
[encryptionKey]
|
||||
);
|
||||
|
||||
logger.info(`Found ${usersResult.rows.length} users with wallets to recheck`);
|
||||
|
||||
// Перепроверяем каждого пользователя
|
||||
for (const user of usersResult.rows) {
|
||||
try {
|
||||
const address = user.address;
|
||||
const currentRole = user.role;
|
||||
|
||||
logger.info(`Rechecking admin status for user ${user.id} with address ${address}`);
|
||||
|
||||
// Проверяем баланс токенов
|
||||
const isAdmin = await checkAdminRole(address);
|
||||
|
||||
// Определяем новую роль
|
||||
const newRole = isAdmin ? 'admin' : 'user';
|
||||
|
||||
// Обновляем роль только если она изменилась
|
||||
if (currentRole !== newRole) {
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, user.id]);
|
||||
logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address})`);
|
||||
} else {
|
||||
logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address})`);
|
||||
}
|
||||
|
||||
} catch (userError) {
|
||||
logger.error(`Error rechecking user ${user.id}: ${userError.message}`);
|
||||
// Продолжаем с другими пользователями
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Completed recheck of admin status for all users');
|
||||
} catch (error) {
|
||||
logger.error(`Error in recheckAllUsersAdminStatus: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка старых гостевых идентификаторов
|
||||
* @param {number} userId - ID пользователя
|
||||
|
||||
@@ -313,18 +313,6 @@ class EmailBotService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не обрабатывали ли мы уже это письмо
|
||||
if (messageId) {
|
||||
const existingMessage = await encryptedDb.getData('messages', {
|
||||
metadata: { $like: `%"messageId":"${messageId}"%` }
|
||||
}, 1);
|
||||
|
||||
if (existingMessage.length > 0) {
|
||||
logger.info(`[EmailBot] Письмо с Message-ID ${messageId} уже обработано, пропускаем`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
||||
if (await isUserBlocked(userId)) {
|
||||
@@ -332,6 +320,31 @@ class EmailBotService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не обрабатывали ли мы уже это письмо
|
||||
if (messageId) {
|
||||
// Проверка дубликатов на основе Message-ID
|
||||
try {
|
||||
const existingMessage = await encryptedDb.getData(
|
||||
'messages',
|
||||
{
|
||||
user_id: userId,
|
||||
channel: 'email',
|
||||
direction: 'in',
|
||||
message_id: messageId
|
||||
},
|
||||
1
|
||||
);
|
||||
|
||||
if (existingMessage.length > 0) {
|
||||
logger.info(`[EmailBot] Игнорируем дубликат письма от ${fromEmail} (Message-ID: ${messageId})`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[EmailBot] Ошибка при проверке дубликатов: ${error.message}`);
|
||||
// Продолжаем обработку в случае ошибки
|
||||
}
|
||||
}
|
||||
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await encryptedDb.getData(
|
||||
'conversations',
|
||||
@@ -376,13 +389,7 @@ class EmailBotService {
|
||||
attachment_mimetype: att.contentType,
|
||||
attachment_size: att.size,
|
||||
attachment_data: att.content,
|
||||
metadata: JSON.stringify({
|
||||
subject,
|
||||
html,
|
||||
messageId: messageId,
|
||||
uid: uid,
|
||||
fromEmail: fromEmail
|
||||
})
|
||||
message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted)
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -398,13 +405,7 @@ class EmailBotService {
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
metadata: JSON.stringify({
|
||||
subject,
|
||||
html,
|
||||
messageId: messageId,
|
||||
uid: uid,
|
||||
fromEmail: fromEmail
|
||||
})
|
||||
message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted)
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -421,7 +422,7 @@ class EmailBotService {
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text });
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
aiResponse = await generateLLMResponse({
|
||||
|
||||
@@ -410,9 +410,10 @@ class EncryptedDataService {
|
||||
*/
|
||||
shouldEncryptColumn(column) {
|
||||
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
|
||||
const excludedColumns = ['created_at', 'updated_at', 'id', 'metadata']; // Добавляем metadata в исключения
|
||||
return encryptableTypes.includes(column.data_type) &&
|
||||
!column.column_name.includes('_encrypted') &&
|
||||
!['created_at', 'updated_at', 'id'].includes(column.column_name);
|
||||
!excludedColumns.includes(column.column_name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -122,7 +122,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
|
||||
// Поиск
|
||||
let results = [];
|
||||
if (rowsForUpsert.length > 0) {
|
||||
results = await vectorSearch.search(tableId, userQuestion, 2); // Уменьшаем до 2 результатов
|
||||
results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска
|
||||
// console.log(`[RAG] Search completed, got ${results.length} results`);
|
||||
|
||||
// Подробное логирование результатов поиска
|
||||
@@ -171,7 +171,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
|
||||
product: best?.metadata?.product,
|
||||
priority: best?.metadata?.priority,
|
||||
date: best?.metadata?.date,
|
||||
score: best?.score,
|
||||
score: best?.score !== undefined && best?.score !== null ? Number(best.score) : null,
|
||||
};
|
||||
|
||||
// Кэшируем результат
|
||||
@@ -188,17 +188,48 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
|
||||
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
||||
*/
|
||||
async function getAllPlaceholdersWithValues() {
|
||||
// Получаем все плейсхолдеры и их значения (берём первое значение для каждого плейсхолдера)
|
||||
const result = await encryptedDb.getData('user_columns', {});
|
||||
|
||||
// Группируем по плейсхолдеру (берём первое значение)
|
||||
const map = {};
|
||||
for (const row of result) {
|
||||
if (row.placeholder && !(row.placeholder in map)) {
|
||||
map[row.placeholder] = row.value;
|
||||
try {
|
||||
console.log('[RAG] Начинаем загрузку плейсхолдеров...');
|
||||
|
||||
// Получаем все колонки с плейсхолдерами
|
||||
const columns = await encryptedDb.getData('user_columns', {});
|
||||
console.log(`[RAG] Получено колонок: ${columns.length}`);
|
||||
|
||||
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder.trim() !== '');
|
||||
console.log(`[RAG] Колонок с плейсхолдерами: ${columnsWithPlaceholders.length}`);
|
||||
|
||||
if (columnsWithPlaceholders.length === 0) {
|
||||
console.log('[RAG] Нет колонок с плейсхолдерами');
|
||||
return {};
|
||||
}
|
||||
|
||||
// Получаем значения для каждой колонки с плейсхолдером
|
||||
const map = {};
|
||||
for (const column of columnsWithPlaceholders) {
|
||||
try {
|
||||
console.log(`[RAG] Получаем значение для плейсхолдера: ${column.placeholder} (column_id: ${column.id})`);
|
||||
|
||||
// Получаем первое значение для этой колонки
|
||||
const values = await encryptedDb.getData('user_cell_values', { column_id: column.id }, 1);
|
||||
console.log(`[RAG] Найдено значений для ${column.placeholder}: ${values ? values.length : 0}`);
|
||||
|
||||
if (values && values.length > 0 && values[0].value) {
|
||||
map[column.placeholder] = values[0].value;
|
||||
console.log(`[RAG] Установлено значение для ${column.placeholder}: ${values[0].value.substring(0, 50)}...`);
|
||||
} else {
|
||||
console.log(`[RAG] Нет значений для плейсхолдера ${column.placeholder}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[RAG] Ошибка получения значения для плейсхолдера ${column.placeholder}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[RAG] Итоговый объект плейсхолдеров:`, Object.keys(map));
|
||||
return map;
|
||||
} catch (error) {
|
||||
console.error('[RAG] Ошибка получения плейсхолдеров:', error);
|
||||
return {};
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,67 +266,222 @@ async function generateLLMResponse({
|
||||
date,
|
||||
rules,
|
||||
history,
|
||||
model,
|
||||
language
|
||||
model
|
||||
}) {
|
||||
// console.log(`[RAG] generateLLMResponse called with:`, {
|
||||
// userQuestion,
|
||||
// context,
|
||||
// answer,
|
||||
// systemPrompt,
|
||||
// userTags,
|
||||
// product,
|
||||
// priority,
|
||||
// date,
|
||||
// model,
|
||||
// language
|
||||
// });
|
||||
console.log(`[RAG] generateLLMResponse called with:`, {
|
||||
userQuestion,
|
||||
context,
|
||||
answer,
|
||||
systemPrompt: systemPrompt ? systemPrompt.substring(0, 100) + '...' : 'null',
|
||||
userTags,
|
||||
product,
|
||||
priority,
|
||||
date,
|
||||
model,
|
||||
historyLength: history ? history.length : 0
|
||||
});
|
||||
|
||||
try {
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
|
||||
// Формируем промпт для LLM
|
||||
let prompt = userQuestion;
|
||||
// Создаем контекст беседы с RAG данными
|
||||
const conversationContext = createConversationContext({
|
||||
userQuestion,
|
||||
ragAnswer: answer,
|
||||
ragContext: context,
|
||||
history,
|
||||
product,
|
||||
priority,
|
||||
date
|
||||
});
|
||||
|
||||
if (context) {
|
||||
prompt += `\n\nКонтекст: ${context}`;
|
||||
// Формируем улучшенный промпт для LLM с учетом найденной информации
|
||||
let prompt = `Вопрос пользователя: ${userQuestion}`;
|
||||
|
||||
// Добавляем найденную информацию из RAG
|
||||
if (answer) {
|
||||
prompt += `\n\nНайденный ответ из базы знаний: ${answer}`;
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
prompt += `\n\nНайденный ответ: ${answer}`;
|
||||
if (context) {
|
||||
prompt += `\n\nДополнительный контекст: ${context}`;
|
||||
}
|
||||
|
||||
if (product) {
|
||||
prompt += `\n\nПродукт: ${product}`;
|
||||
}
|
||||
|
||||
if (priority) {
|
||||
prompt += `\n\nПриоритет: ${priority}`;
|
||||
}
|
||||
|
||||
if (date) {
|
||||
prompt += `\n\nДата: ${date}`;
|
||||
}
|
||||
|
||||
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
||||
let finalSystemPrompt = systemPrompt;
|
||||
if (systemPrompt && systemPrompt.includes('{')) {
|
||||
const placeholders = await getAllPlaceholdersWithValues();
|
||||
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
||||
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
|
||||
}
|
||||
// --- КОНЕЦ ДОБАВЛЕНИЯ ---
|
||||
|
||||
// Получаем ответ от AI
|
||||
const llmResponse = await aiAssistant.getResponse(
|
||||
prompt,
|
||||
language || 'auto',
|
||||
history,
|
||||
finalSystemPrompt,
|
||||
rules
|
||||
);
|
||||
// Используем системный промпт из настроек, если он есть
|
||||
if (finalSystemPrompt && finalSystemPrompt.trim()) {
|
||||
prompt += `\n\nСистемная инструкция: ${finalSystemPrompt}`;
|
||||
} else {
|
||||
// Fallback инструкция, если системный промпт не настроен
|
||||
prompt += `\n\nИнструкция: Используй найденную информацию из базы знаний для ответа. Если найденный ответ подходит к вопросу пользователя, используй его как основу. Если нужно дополнить или уточнить ответ, сделай это. Поддерживай естественную беседу, учитывая предыдущие сообщения. Отвечай на русском языке кратко и по делу. Если пользователь задает уточняющие вопросы, используй контекст предыдущих ответов.`;
|
||||
}
|
||||
|
||||
// console.log(`[RAG] LLM response generated:`, llmResponse);
|
||||
console.log(`[RAG] Сформированный промпт:`, prompt.substring(0, 200) + '...');
|
||||
|
||||
// Получаем ответ от AI с учетом истории беседы
|
||||
let llmResponse;
|
||||
try {
|
||||
llmResponse = await aiAssistant.getResponse(
|
||||
prompt,
|
||||
history,
|
||||
finalSystemPrompt,
|
||||
rules
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[RAG] Error in getResponse:`, error.message);
|
||||
|
||||
// Fallback: если очередь перегружена, возвращаем найденный ответ напрямую
|
||||
if (error.message.includes('очередь перегружена') && answer) {
|
||||
console.log(`[RAG] Queue overloaded, returning direct answer from RAG`);
|
||||
return answer;
|
||||
}
|
||||
|
||||
// Другой fallback для других ошибок
|
||||
return 'Извините, произошла ошибка при генерации ответа.';
|
||||
}
|
||||
|
||||
console.log(`[RAG] LLM response generated:`, llmResponse ? llmResponse.substring(0, 100) + '...' : 'null');
|
||||
return llmResponse;
|
||||
} catch (error) {
|
||||
// console.error(`[RAG] Error generating LLM response:`, error);
|
||||
console.error(`[RAG] Error generating LLM response:`, error);
|
||||
return 'Извините, произошла ошибка при генерации ответа.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает контекст беседы с RAG данными
|
||||
*/
|
||||
function createConversationContext({
|
||||
userQuestion,
|
||||
ragAnswer,
|
||||
ragContext,
|
||||
history,
|
||||
product,
|
||||
priority,
|
||||
date
|
||||
}) {
|
||||
const context = {
|
||||
currentQuestion: userQuestion,
|
||||
ragData: {
|
||||
answer: ragAnswer,
|
||||
context: ragContext,
|
||||
product,
|
||||
priority,
|
||||
date
|
||||
},
|
||||
conversationHistory: history || [],
|
||||
hasRagData: !!(ragAnswer || ragContext),
|
||||
isFollowUpQuestion: history && history.length > 0
|
||||
};
|
||||
|
||||
console.log(`[RAG] Создан контекст беседы:`, {
|
||||
hasRagData: context.hasRagData,
|
||||
historyLength: context.conversationHistory.length,
|
||||
isFollowUp: context.isFollowUpQuestion
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Улучшенная функция RAG с поддержкой беседы
|
||||
*/
|
||||
async function ragAnswerWithConversation({
|
||||
tableId,
|
||||
userQuestion,
|
||||
product = null,
|
||||
threshold = 10,
|
||||
history = [],
|
||||
conversationId = null
|
||||
}) {
|
||||
console.log(`[RAG] ragAnswerWithConversation: tableId=${tableId}, question="${userQuestion}", historyLength=${history.length}`);
|
||||
|
||||
// Получаем базовый RAG результат
|
||||
const ragResult = await ragAnswer({ tableId, userQuestion, product, threshold });
|
||||
|
||||
// Анализируем контекст беседы
|
||||
const conversationContext = createConversationContext({
|
||||
userQuestion,
|
||||
ragAnswer: ragResult.answer,
|
||||
ragContext: ragResult.context,
|
||||
history,
|
||||
product: ragResult.product,
|
||||
priority: ragResult.priority,
|
||||
date: ragResult.date
|
||||
});
|
||||
|
||||
// Если это уточняющий вопрос и есть история
|
||||
if (conversationContext.isFollowUpQuestion && conversationContext.hasRagData) {
|
||||
console.log(`[RAG] Обнаружен уточняющий вопрос с RAG данными`);
|
||||
|
||||
// Проверяем, есть ли точный ответ в первом поиске
|
||||
if (ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 200) {
|
||||
console.log(`[RAG] Найден точный ответ (score=${ragResult.score}), модифицируем с учетом контекста беседы`);
|
||||
|
||||
// Модифицируем точный ответ с учетом контекста беседы
|
||||
let contextualAnswer = ragResult.answer;
|
||||
if (history && history.length > 0) {
|
||||
const contextSummary = history.slice(-3).map(msg => msg.content).join(' | ');
|
||||
contextualAnswer = `Контекст: ${contextSummary}\n\nОтвет: ${ragResult.answer}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...ragResult,
|
||||
answer: contextualAnswer,
|
||||
conversationContext,
|
||||
isFollowUp: true
|
||||
};
|
||||
}
|
||||
|
||||
// Модифицируем вопрос с учетом контекста (only if no confident match)
|
||||
const contextualQuestion = `${userQuestion}\n\nКонтекст предыдущих ответов: ${history.map(msg => msg.content).join('\n')}`;
|
||||
|
||||
// Повторяем поиск с контекстуализированным вопросом
|
||||
const contextualRagResult = await ragAnswer({
|
||||
tableId,
|
||||
userQuestion: contextualQuestion,
|
||||
product,
|
||||
threshold
|
||||
});
|
||||
|
||||
// Объединяем результаты
|
||||
return {
|
||||
...contextualRagResult,
|
||||
conversationContext,
|
||||
isFollowUp: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...ragResult,
|
||||
conversationContext,
|
||||
isFollowUp: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ragAnswer,
|
||||
getTableData,
|
||||
generateLLMResponse
|
||||
generateLLMResponse,
|
||||
ragAnswerWithConversation
|
||||
};
|
||||
@@ -428,7 +428,7 @@ async function getBot() {
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
aiResponse = await generateLLMResponse({
|
||||
|
||||
@@ -365,6 +365,43 @@ function broadcastToAllClients(message) {
|
||||
});
|
||||
}
|
||||
|
||||
// Функции для уведомлений об изменениях токенов
|
||||
function broadcastAuthTokenAdded(tokenData) {
|
||||
const message = JSON.stringify({
|
||||
type: 'auth_token_added',
|
||||
data: {
|
||||
token: tokenData,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
broadcastToAllClients(message);
|
||||
}
|
||||
|
||||
function broadcastAuthTokenDeleted(tokenData) {
|
||||
const message = JSON.stringify({
|
||||
type: 'auth_token_deleted',
|
||||
data: {
|
||||
token: tokenData,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
broadcastToAllClients(message);
|
||||
}
|
||||
|
||||
function broadcastAuthTokenUpdated(tokenData) {
|
||||
const message = JSON.stringify({
|
||||
type: 'auth_token_updated',
|
||||
data: {
|
||||
token: tokenData,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
broadcastToAllClients(message);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initWSS,
|
||||
broadcastContactsUpdate,
|
||||
@@ -378,6 +415,9 @@ module.exports = {
|
||||
broadcastProposalCreated,
|
||||
broadcastProposalVoted,
|
||||
broadcastProposalExecuted,
|
||||
broadcastAuthTokenAdded,
|
||||
broadcastAuthTokenDeleted,
|
||||
broadcastAuthTokenUpdated,
|
||||
getConnectedUsers,
|
||||
getStats
|
||||
};
|
||||
Reference in New Issue
Block a user