ваше сообщение коммита
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -128,6 +128,14 @@ id_ed25519
|
|||||||
ssh_host_*
|
ssh_host_*
|
||||||
ssh_config
|
ssh_config
|
||||||
|
|
||||||
|
# SSL Keys and certificates - КРИТИЧЕСКИ ВАЖНО!
|
||||||
|
ssl/keys/
|
||||||
|
ssl/certs/
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.p12
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
@@ -146,6 +154,14 @@ debug_*.js
|
|||||||
test_*.js
|
test_*.js
|
||||||
test-*.js
|
test-*.js
|
||||||
|
|
||||||
|
# Test files - НЕ ПУБЛИКОВАТЬ!
|
||||||
|
**/test-*.js
|
||||||
|
**/test_*.js
|
||||||
|
**/tests/
|
||||||
|
**/test/
|
||||||
|
test/
|
||||||
|
tests/
|
||||||
|
|
||||||
# Hardhat artifacts and cache
|
# Hardhat artifacts and cache
|
||||||
backend/artifacts/
|
backend/artifacts/
|
||||||
backend/cache/
|
backend/cache/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../../../build-info/ca6cf114dd2b9a54ebfddbb4ba9a86a9.json"
|
"buildInfo": "../../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_format": "hh-sol-dbg-1",
|
||||||
"buildInfo": "../../../../build-info/5f658ec7c83a39083e0b58539865c835.json"
|
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_format": "hh-sol-dbg-1",
|
"_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",
|
"_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",
|
"_format": "hh-sol-cache-2",
|
||||||
"files": {
|
"files": {
|
||||||
"/app/contracts/DLE.sol": {
|
"/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/DLE.sol": {
|
||||||
"lastModificationDate": 1753802664167,
|
"lastModificationDate": 1754485037554,
|
||||||
"contentHash": "de19ae5d6875c4b57e17312ebe37ae43",
|
"contentHash": "f121cb518877db715ab5cd2e3ee5ff3a",
|
||||||
"sourceName": "contracts/DLE.sol",
|
"sourceName": "contracts/DLE.sol",
|
||||||
"solcConfig": {
|
"solcConfig": {
|
||||||
"version": "0.8.20",
|
"version": "0.8.20",
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
},
|
},
|
||||||
"imports": [
|
"imports": [
|
||||||
"@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
"@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
||||||
"@openzeppelin/contracts/utils/ReentrancyGuard.sol"
|
"@openzeppelin/contracts/utils/ReentrancyGuard.sol",
|
||||||
|
"@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"
|
||||||
],
|
],
|
||||||
"versionPragmas": [
|
"versionPragmas": [
|
||||||
"^0.8.20"
|
"^0.8.20"
|
||||||
@@ -41,44 +42,7 @@
|
|||||||
"DLE"
|
"DLE"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/app/node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol": {
|
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.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": {
|
|
||||||
"lastModificationDate": 1754306764456,
|
"lastModificationDate": 1754306764456,
|
||||||
"contentHash": "227a6eb2225701c12d9c959b758b6333",
|
"contentHash": "227a6eb2225701c12d9c959b758b6333",
|
||||||
"sourceName": "@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
"sourceName": "@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
||||||
@@ -120,8 +84,45 @@
|
|||||||
"ERC20"
|
"ERC20"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/app/node_modules/@openzeppelin/contracts/utils/Context.sol": {
|
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol": {
|
||||||
"lastModificationDate": 1753876422645,
|
"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",
|
"contentHash": "67bfbc07588eb8683b3fd8f6f909563e",
|
||||||
"sourceName": "@openzeppelin/contracts/utils/Context.sol",
|
"sourceName": "@openzeppelin/contracts/utils/Context.sol",
|
||||||
"solcConfig": {
|
"solcConfig": {
|
||||||
@@ -157,7 +158,7 @@
|
|||||||
"Context"
|
"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,
|
"lastModificationDate": 1754306760460,
|
||||||
"contentHash": "267d92fe4de67b1bdb3302c08f387dbf",
|
"contentHash": "267d92fe4de67b1bdb3302c08f387dbf",
|
||||||
"sourceName": "@openzeppelin/contracts/interfaces/draft-IERC6093.sol",
|
"sourceName": "@openzeppelin/contracts/interfaces/draft-IERC6093.sol",
|
||||||
@@ -196,7 +197,7 @@
|
|||||||
"IERC721Errors"
|
"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,
|
"lastModificationDate": 1754306764456,
|
||||||
"contentHash": "8f19f64d2adadf448840908bbaf431c8",
|
"contentHash": "8f19f64d2adadf448840908bbaf431c8",
|
||||||
"sourceName": "@openzeppelin/contracts/token/ERC20/IERC20.sol",
|
"sourceName": "@openzeppelin/contracts/token/ERC20/IERC20.sol",
|
||||||
@@ -233,7 +234,7 @@
|
|||||||
"IERC20"
|
"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,
|
"lastModificationDate": 1754306768254,
|
||||||
"contentHash": "794db3115001aa372c79326fcfd44b1f",
|
"contentHash": "794db3115001aa372c79326fcfd44b1f",
|
||||||
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
|
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
|
||||||
@@ -271,6 +272,82 @@
|
|||||||
"artifacts": [
|
"artifacts": [
|
||||||
"IERC20Metadata"
|
"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/token/ERC20/ERC20.sol";
|
||||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title DLE (Digital Legal Entity)
|
* @title DLE (Digital Legal Entity)
|
||||||
@@ -59,36 +60,29 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
mapping(uint256 => bool) chainVoteSynced; // Синхронизация голосов между цепочками
|
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;
|
DLEInfo public dleInfo;
|
||||||
uint256 public quorumPercentage;
|
uint256 public quorumPercentage;
|
||||||
uint256 public proposalCounter;
|
uint256 public proposalCounter;
|
||||||
uint256 public multiSigCounter;
|
|
||||||
uint256 public currentChainId;
|
uint256 public currentChainId;
|
||||||
|
|
||||||
// Модули
|
// Модули
|
||||||
mapping(bytes32 => address) public modules;
|
mapping(bytes32 => address) public modules;
|
||||||
mapping(bytes32 => bool) public activeModules;
|
mapping(bytes32 => bool) public activeModules;
|
||||||
|
|
||||||
// Предложения и мультиподписи
|
// Предложения
|
||||||
mapping(uint256 => Proposal) public proposals;
|
mapping(uint256 => Proposal) public proposals;
|
||||||
mapping(uint256 => MultiSigOperation) public multiSigOperations;
|
|
||||||
|
|
||||||
// Мульти-чейн
|
// Мульти-чейн
|
||||||
mapping(uint256 => bool) public supportedChains;
|
mapping(uint256 => bool) public supportedChains;
|
||||||
|
uint256[] public supportedChainIds;
|
||||||
mapping(uint256 => bool) public executedProposals; // Синхронизация исполненных предложений
|
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(
|
event DLEInitialized(
|
||||||
@@ -107,14 +101,16 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
event ProposalCreated(uint256 proposalId, address initiator, string description);
|
event ProposalCreated(uint256 proposalId, address initiator, string description);
|
||||||
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
|
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
|
||||||
event ProposalExecuted(uint256 proposalId, bytes operation);
|
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 ModuleAdded(bytes32 moduleId, address moduleAddress);
|
||||||
event ModuleRemoved(bytes32 moduleId);
|
event ModuleRemoved(bytes32 moduleId);
|
||||||
event CrossChainExecutionSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
|
event CrossChainExecutionSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
|
||||||
event CrossChainVoteSync(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(
|
constructor(
|
||||||
DLEConfig memory config,
|
DLEConfig memory config,
|
||||||
@@ -139,6 +135,7 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
// Настраиваем поддерживаемые цепочки
|
// Настраиваем поддерживаемые цепочки
|
||||||
for (uint256 i = 0; i < config.supportedChainIds.length; i++) {
|
for (uint256 i = 0; i < config.supportedChainIds.length; i++) {
|
||||||
supportedChains[config.supportedChainIds[i]] = true;
|
supportedChains[config.supportedChainIds[i]] = true;
|
||||||
|
supportedChainIds.push(config.supportedChainIds[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Распределяем начальные токены партнерам
|
// Распределяем начальные токены партнерам
|
||||||
@@ -237,15 +234,34 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
uint256 _fromChainId,
|
uint256 _fromChainId,
|
||||||
uint256 _forVotes,
|
uint256 _forVotes,
|
||||||
uint256 _againstVotes,
|
uint256 _againstVotes,
|
||||||
bytes memory /* _proof */
|
bytes memory _proof
|
||||||
) external {
|
) external {
|
||||||
Proposal storage proposal = proposals[_proposalId];
|
Proposal storage proposal = proposals[_proposalId];
|
||||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||||
require(supportedChains[_fromChainId], "Chain not supported");
|
require(supportedChains[_fromChainId], "Chain not supported");
|
||||||
require(!proposal.chainVoteSynced[_fromChainId], "Already synced");
|
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.forVotes += _forVotes;
|
||||||
proposal.againstVotes += _againstVotes;
|
proposal.againstVotes += _againstVotes;
|
||||||
@@ -281,9 +297,15 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
Proposal storage proposal = proposals[_proposalId];
|
Proposal storage proposal = proposals[_proposalId];
|
||||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||||
require(!proposal.executed, "Proposal already executed");
|
require(!proposal.executed, "Proposal already executed");
|
||||||
require(block.timestamp >= proposal.deadline, "Voting not ended");
|
|
||||||
|
|
||||||
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
(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");
|
require(passed && quorumReached, "Proposal not passed");
|
||||||
|
|
||||||
proposal.executed = true;
|
proposal.executed = true;
|
||||||
@@ -294,123 +316,6 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
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 Синхронизировать исполнение из другой цепочки
|
* @dev Синхронизировать исполнение из другой цепочки
|
||||||
* @param _proposalId ID предложения
|
* @param _proposalId ID предложения
|
||||||
@@ -419,18 +324,37 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
function syncExecutionFromChain(
|
function syncExecutionFromChain(
|
||||||
uint256 _proposalId,
|
uint256 _proposalId,
|
||||||
uint256 _fromChainId,
|
uint256 _fromChainId,
|
||||||
bytes memory /* _proof */
|
bytes memory _proof
|
||||||
) external {
|
) external {
|
||||||
require(supportedChains[_fromChainId], "Chain not supported");
|
require(supportedChains[_fromChainId], "Chain not supported");
|
||||||
require(!executedProposals[_proposalId], "Already executed");
|
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;
|
executedProposals[_proposalId] = true;
|
||||||
|
|
||||||
// Получаем операцию из предложения
|
// Исполняем операцию из предложения
|
||||||
Proposal storage proposal = proposals[_proposalId];
|
|
||||||
if (proposal.id == _proposalId) {
|
if (proposal.id == _proposalId) {
|
||||||
_executeOperation(proposal.operation);
|
_executeOperation(proposal.operation);
|
||||||
}
|
}
|
||||||
@@ -444,9 +368,19 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
* @return isAvailable Доступна ли цепочка
|
* @return isAvailable Доступна ли цепочка
|
||||||
*/
|
*/
|
||||||
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
|
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
|
||||||
// В реальной реализации здесь должна быть проверка подключения
|
// Проверяем, поддерживается ли цепочка
|
||||||
// Для примера возвращаем true для поддерживаемых цепочек
|
if (!supportedChains[_chainId]) {
|
||||||
return 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 цепочки
|
* @param _chainId ID цепочки
|
||||||
*/
|
*/
|
||||||
function syncToChain(uint256 _proposalId, uint256 _chainId) internal {
|
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);
|
emit CrossChainExecutionSync(_proposalId, currentChainId, _chainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Получить количество поддерживаемых цепочек
|
* @dev Получить количество поддерживаемых цепочек
|
||||||
*/
|
*/
|
||||||
function getSupportedChainCount() public pure returns (uint256) {
|
function getSupportedChainCount() public view returns (uint256) {
|
||||||
// В реальной реализации нужно хранить массив поддерживаемых цепочек
|
return supportedChainIds.length;
|
||||||
// Для примера возвращаем 4 (Ethereum, Polygon, BSC, Arbitrum)
|
|
||||||
return 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Получить ID поддерживаемой цепочки по индексу
|
* @dev Получить ID поддерживаемой цепочки по индексу
|
||||||
* @param _index Индекс цепочки
|
* @param _index Индекс цепочки
|
||||||
*/
|
*/
|
||||||
function getSupportedChainId(uint256 _index) public pure returns (uint256) {
|
function getSupportedChainId(uint256 _index) public view returns (uint256) {
|
||||||
if (_index == 0) return 1; // Ethereum
|
require(_index < supportedChainIds.length, "Invalid chain index");
|
||||||
if (_index == 1) return 137; // Polygon
|
return supportedChainIds[_index];
|
||||||
if (_index == 2) return 56; // BSC
|
}
|
||||||
if (_index == 3) return 42161; // Arbitrum
|
|
||||||
revert("Invalid chain 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));
|
(address from, uint256 amount) = abi.decode(data, (address, uint256));
|
||||||
_burn(from, amount);
|
_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 {
|
} else {
|
||||||
// Неизвестная операция
|
// Неизвестная операция
|
||||||
revert("Unknown operation");
|
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 _moduleId ID модуля
|
||||||
* @param _moduleAddress Адрес модуля
|
* @param _moduleAddress Адрес модуля
|
||||||
*/
|
*/
|
||||||
function addModule(bytes32 _moduleId, address _moduleAddress) external {
|
function _addModule(bytes32 _moduleId, address _moduleAddress) internal {
|
||||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to add module");
|
|
||||||
require(_moduleAddress != address(0), "Zero address");
|
require(_moduleAddress != address(0), "Zero address");
|
||||||
require(!activeModules[_moduleId], "Module already exists");
|
require(!activeModules[_moduleId], "Module already exists");
|
||||||
|
|
||||||
@@ -560,11 +732,10 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Удалить модуль
|
* @dev Удалить модуль (внутренняя функция, вызывается через кворум)
|
||||||
* @param _moduleId ID модуля
|
* @param _moduleId ID модуля
|
||||||
*/
|
*/
|
||||||
function removeModule(bytes32 _moduleId) external {
|
function _removeModule(bytes32 _moduleId) internal {
|
||||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to remove module");
|
|
||||||
require(activeModules[_moduleId], "Module does not exist");
|
require(activeModules[_moduleId], "Module does not exist");
|
||||||
|
|
||||||
delete modules[_moduleId];
|
delete modules[_moduleId];
|
||||||
@@ -613,4 +784,190 @@ contract DLE is ERC20, ReentrancyGuard {
|
|||||||
|
|
||||||
// События для новых функций
|
// События для новых функций
|
||||||
event SyncCompleted(uint256 proposalId);
|
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 = {
|
const taskData = {
|
||||||
message,
|
message,
|
||||||
language: language || 'auto',
|
|
||||||
history: history || null,
|
history: history || null,
|
||||||
systemPrompt: systemPrompt || '',
|
systemPrompt: systemPrompt || '',
|
||||||
rules: rules || null,
|
rules: rules || null,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
const blockchainData = {
|
const blockchainData = {
|
||||||
name: dleInfo.name,
|
name: dleInfo.name,
|
||||||
symbol: dleInfo.symbol,
|
symbol: dleInfo.symbol,
|
||||||
|
dleAddress: dleAddress, // Добавляем адрес контракта
|
||||||
location: dleInfo.location,
|
location: dleInfo.location,
|
||||||
coordinates: dleInfo.coordinates,
|
coordinates: dleInfo.coordinates,
|
||||||
jurisdiction: Number(dleInfo.jurisdiction),
|
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
|
// Импортируем WebSocket функции из wsHub
|
||||||
const { broadcastProposalCreated, broadcastProposalVoted, broadcastProposalExecuted } = require('../wsHub');
|
const { broadcastProposalCreated, broadcastProposalVoted, broadcastProposalExecuted } = require('../wsHub');
|
||||||
|
|
||||||
|
|||||||
@@ -171,12 +171,9 @@ async function processGuestMessages(userId, guestId) {
|
|||||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
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);
|
logger.info('Getting AI response for guest message:', guestMessage.content);
|
||||||
const aiResponseContent = await aiAssistant.getResponse(
|
const aiResponseContent = await aiAssistant.getResponse(
|
||||||
guestMessage.content,
|
guestMessage.content,
|
||||||
detectedLanguage,
|
|
||||||
history,
|
history,
|
||||||
aiSettings ? aiSettings.system_prompt : '',
|
aiSettings ? aiSettings.system_prompt : '',
|
||||||
rules ? rules.rules : null
|
rules ? rules.rules : null
|
||||||
@@ -310,7 +307,7 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
|||||||
[
|
[
|
||||||
guestId,
|
guestId,
|
||||||
messageContent, // Текст сообщения или NULL
|
messageContent, // Текст сообщения или NULL
|
||||||
language || 'auto',
|
'ru', // Устанавливаем русский язык по умолчанию
|
||||||
attachmentFilename,
|
attachmentFilename,
|
||||||
attachmentMimetype,
|
attachmentMimetype,
|
||||||
attachmentSize,
|
attachmentSize,
|
||||||
@@ -511,7 +508,14 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
if (aiSettings && aiSettings.rules_id) {
|
if (aiSettings && aiSettings.rules_id) {
|
||||||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||||
}
|
}
|
||||||
// --- RAG автоответ ---
|
// --- RAG автоответ с поддержкой беседы ---
|
||||||
|
// Пример работы:
|
||||||
|
// 1. Пользователь: "Как подключить кошелек?"
|
||||||
|
// RAG: находит точный ответ → возвращает его
|
||||||
|
// 2. Пользователь: "А какие документы нужны?"
|
||||||
|
// RAG: анализирует контекст предыдущего ответа → ищет информацию о документах
|
||||||
|
// 3. Пользователь: "Сколько это займет времени?"
|
||||||
|
// RAG: использует полный контекст беседы → дает уточненный ответ
|
||||||
let ragTableId = null;
|
let ragTableId = null;
|
||||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||||
ragTableId = Array.isArray(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;
|
let ragResult = null;
|
||||||
if (ragTableId) {
|
if (ragTableId) {
|
||||||
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
|
const { ragAnswerWithConversation, generateLLMResponse } = require('../services/ragService');
|
||||||
const threshold = 0.3;
|
const threshold = 200; // Увеличиваем threshold для более широкого поиска
|
||||||
logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
|
|
||||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, 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] Результат поиска по 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) {
|
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= threshold) {
|
||||||
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
||||||
// Прямой ответ из RAG
|
// Прямой ответ из RAG
|
||||||
@@ -542,15 +564,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
broadcastChatMessage(aiMessage);
|
broadcastChatMessage(aiMessage);
|
||||||
} else if (ragResult) {
|
} else if (ragResult) {
|
||||||
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
||||||
// Генерация через LLM с подстановкой значений из RAG
|
// Генерация через 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
|
|
||||||
}));
|
|
||||||
const llmResponse = await generateLLMResponse({
|
const llmResponse = await generateLLMResponse({
|
||||||
userQuestion: messageContent,
|
userQuestion: messageContent,
|
||||||
context: ragResult.context,
|
context: ragResult.context,
|
||||||
@@ -558,9 +572,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
clarifyingAnswer: ragResult.clarifyingAnswer,
|
clarifyingAnswer: ragResult.clarifyingAnswer,
|
||||||
objectionAnswer: ragResult.objectionAnswer,
|
objectionAnswer: ragResult.objectionAnswer,
|
||||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||||
history,
|
history: ragResult.conversationContext ? ragResult.conversationContext.conversationHistory : history,
|
||||||
model: aiSettings ? aiSettings.model : undefined,
|
model: aiSettings ? aiSettings.model : undefined
|
||||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
|
||||||
});
|
});
|
||||||
if (llmResponse) {
|
if (llmResponse) {
|
||||||
aiMessage = await encryptedDb.saveData('messages', {
|
aiMessage = await encryptedDb.saveData('messages', {
|
||||||
@@ -824,7 +837,6 @@ router.post('/message-queued', requireAuth, upload.array('attachments'), async (
|
|||||||
// Добавляем задачу в очередь
|
// Добавляем задачу в очередь
|
||||||
const taskData = {
|
const taskData = {
|
||||||
message: messageContent,
|
message: messageContent,
|
||||||
language: language || 'auto',
|
|
||||||
history: history,
|
history: history,
|
||||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||||
rules: rules,
|
rules: rules,
|
||||||
@@ -927,7 +939,10 @@ router.get('/history', requireAuth, async (req, res) => {
|
|||||||
whereConditions.conversation_id = conversationId;
|
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 => {
|
const formattedMessages = messages.map(msg => {
|
||||||
@@ -1057,7 +1072,6 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
|
|||||||
logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
|
logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
|
||||||
}
|
}
|
||||||
const { generateLLMResponse } = require('../services/ragService');
|
const { generateLLMResponse } = require('../services/ragService');
|
||||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
|
||||||
const aiResponseContent = await generateLLMResponse({
|
const aiResponseContent = await generateLLMResponse({
|
||||||
userQuestion: promptText,
|
userQuestion: promptText,
|
||||||
context: ragResult && ragResult.context ? ragResult.context : '',
|
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||||
@@ -1065,7 +1079,6 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
|
|||||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||||
history,
|
history,
|
||||||
model: aiSettings ? aiSettings.model : undefined,
|
model: aiSettings ? aiSettings.model : undefined,
|
||||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru',
|
|
||||||
rules: rules ? rules.rules : null
|
rules: rules ? rules.rules : null
|
||||||
});
|
});
|
||||||
res.json({ success: true, aiMessage: aiResponseContent });
|
res.json({ success: true, aiMessage: aiResponseContent });
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const telegramBot = require('../services/telegramBot');
|
|||||||
const EmailBotService = require('../services/emailBot');
|
const EmailBotService = require('../services/emailBot');
|
||||||
const emailBotService = new EmailBotService();
|
const emailBotService = new EmailBotService();
|
||||||
const dbSettingsService = require('../services/dbSettingsService');
|
const dbSettingsService = require('../services/dbSettingsService');
|
||||||
|
const { broadcastAuthTokenAdded, broadcastAuthTokenDeleted, broadcastAuthTokenUpdated } = require('../wsHub');
|
||||||
|
|
||||||
// Логируем версию ethers для отладки
|
// Логируем версию ethers для отладки
|
||||||
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
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: 'Неверный формат данных' });
|
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
||||||
}
|
}
|
||||||
await authTokenService.saveAllAuthTokens(authTokens);
|
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: 'Токены аутентификации успешно сохранены' });
|
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при сохранении токенов аутентификации:', 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 обязательны' });
|
return res.status(400).json({ success: false, error: 'name, address и network обязательны' });
|
||||||
}
|
}
|
||||||
await authTokenService.upsertAuthToken({ name, address, network, minBalance });
|
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: 'Токен аутентификации сохранён' });
|
res.json({ success: true, message: 'Токен аутентификации сохранён' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при сохранении токена аутентификации:', error);
|
logger.error('Ошибка при сохранении токена аутентификации:', error);
|
||||||
@@ -190,6 +219,24 @@ router.delete('/auth-token/:address/:network', requireAdmin, async (req, res, ne
|
|||||||
try {
|
try {
|
||||||
const { address, network } = req.params;
|
const { address, network } = req.params;
|
||||||
await authTokenService.deleteAuthToken(address, network);
|
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: 'Токен аутентификации удалён' });
|
res.json({ success: true, message: 'Токен аутентификации удалён' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при удалении токена аутентификации:', error);
|
logger.error('Ошибка при удалении токена аутентификации:', error);
|
||||||
|
|||||||
@@ -70,43 +70,50 @@ class AIAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создание экземпляра ChatOllama с нужными параметрами
|
// Создание экземпляра ChatOllama с нужными параметрами
|
||||||
createChat(language = 'ru', customSystemPrompt = '') {
|
createChat(customSystemPrompt = '') {
|
||||||
// Используем кастомный системный промпт, если он передан, иначе используем дефолтный
|
// Используем кастомный системный промпт, если он передан, иначе используем дефолтный
|
||||||
let systemPrompt = customSystemPrompt;
|
let systemPrompt = customSystemPrompt;
|
||||||
if (!systemPrompt) {
|
if (!systemPrompt) {
|
||||||
systemPrompt = language === 'ru'
|
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.';
|
||||||
? 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.'
|
|
||||||
: 'You are a helpful assistant. Respond in English briefly and to the point.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChatOllama({
|
return new ChatOllama({
|
||||||
baseUrl: this.baseUrl,
|
baseUrl: this.baseUrl,
|
||||||
model: this.defaultModel,
|
model: this.defaultModel,
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
temperature: 0.3, // Уменьшаем для более предсказуемых ответов
|
temperature: 0.7, // Восстанавливаем для более творческих ответов
|
||||||
maxTokens: 100, // Еще больше уменьшаем для быстрого ответа
|
maxTokens: 2048, // Восстанавливаем для полных ответов
|
||||||
timeout: 60000, // Увеличиваем таймаут до 60 секунд
|
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: {
|
options: {
|
||||||
num_ctx: 512, // Еще больше уменьшаем контекст для экономии памяти
|
numCtx: 4096,
|
||||||
num_thread: 12, // Увеличиваем количество потоков еще больше
|
numGpu: 1,
|
||||||
num_gpu: 1,
|
numThread: 8,
|
||||||
num_gqa: 8,
|
repeatPenalty: 1.1,
|
||||||
rope_freq_base: 1000000,
|
topK: 40,
|
||||||
rope_freq_scale: 0.5,
|
topP: 0.9,
|
||||||
repeat_penalty: 1.1, // Добавляем штраф за повторения
|
tfsZ: 1,
|
||||||
top_k: 20, // Еще больше ограничиваем выбор токенов
|
mirostat: 2,
|
||||||
top_p: 0.8, // Уменьшаем nucleus sampling
|
mirostatTau: 5,
|
||||||
temperature: 0.1, // Еще больше уменьшаем для более предсказуемых ответов
|
mirostatEta: 0.1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определение языка сообщения
|
|
||||||
detectLanguage(message) {
|
|
||||||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
|
||||||
return cyrillicPattern.test(message) ? 'ru' : 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Определение приоритета запроса
|
// Определение приоритета запроса
|
||||||
getRequestPriority(message, history, rules) {
|
getRequestPriority(message, history, rules) {
|
||||||
let priority = 0;
|
let priority = 0;
|
||||||
@@ -117,7 +124,7 @@ class AIAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Приоритет по типу запроса
|
// Приоритет по типу запроса
|
||||||
const urgentKeywords = ['срочно', 'urgent', 'важно', 'important', 'помоги', 'help'];
|
const urgentKeywords = ['срочно', 'важно', 'помоги'];
|
||||||
if (urgentKeywords.some(keyword => message.toLowerCase().includes(keyword))) {
|
if (urgentKeywords.some(keyword => message.toLowerCase().includes(keyword))) {
|
||||||
priority += 20;
|
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 {
|
try {
|
||||||
// console.log('getResponse called with:', { message, language, history, systemPrompt, rules });
|
// console.log('getResponse called with:', { message, history, systemPrompt, rules });
|
||||||
|
|
||||||
// Очищаем старый кэш
|
// Очищаем старый кэш
|
||||||
this.cleanupCache();
|
this.cleanupCache();
|
||||||
@@ -171,7 +178,6 @@ class AIAssistant {
|
|||||||
// Добавляем запрос в очередь
|
// Добавляем запрос в очередь
|
||||||
const requestId = await aiQueue.addRequest({
|
const requestId = await aiQueue.addRequest({
|
||||||
message,
|
message,
|
||||||
language,
|
|
||||||
history,
|
history,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
rules
|
rules
|
||||||
@@ -181,7 +187,7 @@ class AIAssistant {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reject(new Error('Request timeout - очередь перегружена'));
|
reject(new Error('Request timeout - очередь перегружена'));
|
||||||
}, 60000); // 60 секунд таймаут для очереди
|
}, 180000); // 180 секунд таймаут для очереди (увеличено с 60)
|
||||||
|
|
||||||
const onCompleted = (item) => {
|
const onCompleted = (item) => {
|
||||||
if (item.id === requestId) {
|
if (item.id === requestId) {
|
||||||
@@ -204,62 +210,6 @@ class AIAssistant {
|
|||||||
aiQueue.on('completed', onCompleted);
|
aiQueue.on('completed', onCompleted);
|
||||||
aiQueue.on('failed', onFailed);
|
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) {
|
} catch (error) {
|
||||||
// console.error('Error in getResponse:', error);
|
// console.error('Error in getResponse:', error);
|
||||||
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
||||||
@@ -267,9 +217,9 @@ class AIAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
|
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
|
||||||
async fallbackRequestOpenAI(messages, language, systemPrompt = '') {
|
async fallbackRequestOpenAI(messages, systemPrompt = '') {
|
||||||
try {
|
try {
|
||||||
// console.log('Using fallbackRequestOpenAI with:', { messages, language, systemPrompt });
|
// console.log('Using fallbackRequestOpenAI with:', { messages, systemPrompt });
|
||||||
const model = this.defaultModel;
|
const model = this.defaultModel;
|
||||||
|
|
||||||
// Создаем AbortController для таймаута
|
// Создаем AbortController для таймаута
|
||||||
@@ -284,23 +234,25 @@ class AIAssistant {
|
|||||||
messages,
|
messages,
|
||||||
stream: false,
|
stream: false,
|
||||||
options: {
|
options: {
|
||||||
temperature: 0.3,
|
temperature: 0.7,
|
||||||
num_predict: 150, // Уменьшаем максимальную длину ответа для ускорения
|
num_predict: 2048, // Восстанавливаем для полных ответов
|
||||||
num_ctx: 512, // Уменьшаем контекст для экономии памяти и ускорения
|
num_ctx: 4096, // Восстанавливаем контекст для лучшего понимания
|
||||||
num_thread: 12, // Увеличиваем количество потоков для ускорения
|
num_thread: 8, // Оптимальное количество потоков
|
||||||
num_gpu: 1, // Используем GPU если доступен
|
num_gpu: 1, // Используем GPU если доступен
|
||||||
num_gqa: 8, // Оптимизация для qwen2.5
|
num_gqa: 8, // Оптимизация для qwen2.5
|
||||||
rope_freq_base: 1000000, // Оптимизация для qwen2.5
|
rope_freq_base: 1000000, // Оптимизация для qwen2.5
|
||||||
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
|
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
|
||||||
repeat_penalty: 1.1, // Добавляем штраф за повторения
|
repeat_penalty: 1.1, // Восстанавливаем штраф за повторения
|
||||||
top_k: 20, // Уменьшаем выбор токенов для ускорения
|
top_k: 40, // Восстанавливаем разнообразие ответов
|
||||||
top_p: 0.8, // Уменьшаем nucleus sampling для ускорения
|
top_p: 0.9, // Восстанавливаем nucleus sampling
|
||||||
mirostat: 2, // Используем mirostat для стабильности
|
tfs_z: 1, // Tail free sampling
|
||||||
mirostat_tau: 5.0, // Настройка mirostat
|
mirostat: 2, // Mirostat 2.0 для контроля качества
|
||||||
mirostat_eta: 0.1, // Настройка mirostat
|
mirostat_tau: 5, // Целевая перплексия
|
||||||
},
|
mirostat_eta: 0.1, // Скорость адаптации
|
||||||
}),
|
seed: -1, // Случайный сид
|
||||||
signal: controller.signal,
|
stop: [] // Стоп-слова
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class AIQueue extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.maxConcurrent = 2; // Максимум 2 запроса одновременно
|
this.maxConcurrent = 1; // Максимум 1 запрос одновременно (последовательная обработка)
|
||||||
this.activeRequests = 0;
|
this.activeRequests = 0;
|
||||||
this.stats = {
|
this.stats = {
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -51,6 +51,7 @@ class AIQueue extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
logger.info(`[AIQueue] Начинаем обработку очереди. Запросов в очереди: ${this.queue.length}`);
|
||||||
|
|
||||||
while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
|
while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
|
||||||
const item = this.queue.shift();
|
const item = this.queue.shift();
|
||||||
@@ -58,6 +59,7 @@ class AIQueue extends EventEmitter {
|
|||||||
|
|
||||||
this.activeRequests++;
|
this.activeRequests++;
|
||||||
item.status = 'processing';
|
item.status = 'processing';
|
||||||
|
logger.info(`[AIQueue] Обрабатываем запрос ${item.id} (приоритет: ${item.priority})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -71,7 +73,7 @@ class AIQueue extends EventEmitter {
|
|||||||
this.stats.completed++;
|
this.stats.completed++;
|
||||||
this.updateAvgResponseTime(responseTime);
|
this.updateAvgResponseTime(responseTime);
|
||||||
|
|
||||||
logger.info(`[AIQueue] Request ${item.id} completed in ${responseTime}ms`);
|
logger.info(`[AIQueue] Запрос ${item.id} завершен за ${responseTime}ms`);
|
||||||
|
|
||||||
// Эмитим событие о завершении
|
// Эмитим событие о завершении
|
||||||
this.emit('completed', item);
|
this.emit('completed', item);
|
||||||
@@ -81,7 +83,7 @@ class AIQueue extends EventEmitter {
|
|||||||
item.error = error.message;
|
item.error = error.message;
|
||||||
|
|
||||||
this.stats.failed++;
|
this.stats.failed++;
|
||||||
logger.error(`[AIQueue] Request ${item.id} failed:`, error.message);
|
logger.error(`[AIQueue] Запрос ${item.id} завершился с ошибкой:`, error.message);
|
||||||
|
|
||||||
// Эмитим событие об ошибке
|
// Эмитим событие об ошибке
|
||||||
this.emit('failed', item);
|
this.emit('failed', item);
|
||||||
@@ -91,6 +93,7 @@ class AIQueue extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
logger.info(`[AIQueue] Обработка очереди завершена. Осталось запросов: ${this.queue.length}`);
|
||||||
|
|
||||||
// Если в очереди еще есть запросы, продолжаем обработку
|
// Если в очереди еще есть запросы, продолжаем обработку
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
@@ -118,7 +121,7 @@ class AIQueue extends EventEmitter {
|
|||||||
messages.push({ role: 'user', content: request.message });
|
messages.push({ role: 'user', content: request.message });
|
||||||
|
|
||||||
// Прямой вызов API без очереди
|
// Прямой вызов 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;
|
supportEmail = em.rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...setting,
|
...setting,
|
||||||
telegramBot,
|
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 = {
|
const data = {
|
||||||
id: 1,
|
id: 1,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
selected_rag_tables,
|
selected_rag_tables,
|
||||||
languages,
|
languages: ['ru'], // Устанавливаем русский язык по умолчанию
|
||||||
model,
|
model,
|
||||||
embedding_model,
|
embedding_model,
|
||||||
rules,
|
rules,
|
||||||
|
|||||||
@@ -519,6 +519,20 @@ class AuthService {
|
|||||||
} else {
|
} else {
|
||||||
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
|
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
|
||||||
try {
|
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()(
|
const userResult = await db.getQuery()(
|
||||||
`
|
`
|
||||||
SELECT u.id, u.role FROM users u
|
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 пользователя
|
* @param {number} userId - ID пользователя
|
||||||
|
|||||||
@@ -313,18 +313,6 @@ class EmailBotService {
|
|||||||
return;
|
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. Найти или создать пользователя
|
// 1. Найти или создать пользователя
|
||||||
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
||||||
if (await isUserBlocked(userId)) {
|
if (await isUserBlocked(userId)) {
|
||||||
@@ -332,6 +320,31 @@ class EmailBotService {
|
|||||||
return;
|
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 Найти или создать беседу
|
// 1.1 Найти или создать беседу
|
||||||
let conversationResult = await encryptedDb.getData(
|
let conversationResult = await encryptedDb.getData(
|
||||||
'conversations',
|
'conversations',
|
||||||
@@ -376,13 +389,7 @@ class EmailBotService {
|
|||||||
attachment_mimetype: att.contentType,
|
attachment_mimetype: att.contentType,
|
||||||
attachment_size: att.size,
|
attachment_size: att.size,
|
||||||
attachment_data: att.content,
|
attachment_data: att.content,
|
||||||
metadata: JSON.stringify({
|
message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted)
|
||||||
subject,
|
|
||||||
html,
|
|
||||||
messageId: messageId,
|
|
||||||
uid: uid,
|
|
||||||
fromEmail: fromEmail
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -398,13 +405,7 @@ class EmailBotService {
|
|||||||
role: role,
|
role: role,
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
metadata: JSON.stringify({
|
message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted)
|
||||||
subject,
|
|
||||||
html,
|
|
||||||
messageId: messageId,
|
|
||||||
uid: uid,
|
|
||||||
fromEmail: fromEmail
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -421,7 +422,7 @@ class EmailBotService {
|
|||||||
if (ragTableId) {
|
if (ragTableId) {
|
||||||
// Сначала ищем ответ через RAG
|
// Сначала ищем ответ через RAG
|
||||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text });
|
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;
|
aiResponse = ragResult.answer;
|
||||||
} else {
|
} else {
|
||||||
aiResponse = await generateLLMResponse({
|
aiResponse = await generateLLMResponse({
|
||||||
|
|||||||
@@ -410,9 +410,10 @@ class EncryptedDataService {
|
|||||||
*/
|
*/
|
||||||
shouldEncryptColumn(column) {
|
shouldEncryptColumn(column) {
|
||||||
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
|
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
|
||||||
|
const excludedColumns = ['created_at', 'updated_at', 'id', 'metadata']; // Добавляем metadata в исключения
|
||||||
return encryptableTypes.includes(column.data_type) &&
|
return encryptableTypes.includes(column.data_type) &&
|
||||||
!column.column_name.includes('_encrypted') &&
|
!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 = [];
|
let results = [];
|
||||||
if (rowsForUpsert.length > 0) {
|
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`);
|
// 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,
|
product: best?.metadata?.product,
|
||||||
priority: best?.metadata?.priority,
|
priority: best?.metadata?.priority,
|
||||||
date: best?.metadata?.date,
|
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, ... }
|
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
||||||
*/
|
*/
|
||||||
async function getAllPlaceholdersWithValues() {
|
async function getAllPlaceholdersWithValues() {
|
||||||
// Получаем все плейсхолдеры и их значения (берём первое значение для каждого плейсхолдера)
|
try {
|
||||||
const result = await encryptedDb.getData('user_columns', {});
|
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 = {};
|
const map = {};
|
||||||
for (const row of result) {
|
for (const column of columnsWithPlaceholders) {
|
||||||
if (row.placeholder && !(row.placeholder in map)) {
|
try {
|
||||||
map[row.placeholder] = row.value;
|
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;
|
return map;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RAG] Ошибка получения плейсхолдеров:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,67 +266,222 @@ async function generateLLMResponse({
|
|||||||
date,
|
date,
|
||||||
rules,
|
rules,
|
||||||
history,
|
history,
|
||||||
model,
|
model
|
||||||
language
|
|
||||||
}) {
|
}) {
|
||||||
// console.log(`[RAG] generateLLMResponse called with:`, {
|
console.log(`[RAG] generateLLMResponse called with:`, {
|
||||||
// userQuestion,
|
userQuestion,
|
||||||
// context,
|
context,
|
||||||
// answer,
|
answer,
|
||||||
// systemPrompt,
|
systemPrompt: systemPrompt ? systemPrompt.substring(0, 100) + '...' : 'null',
|
||||||
// userTags,
|
userTags,
|
||||||
// product,
|
product,
|
||||||
// priority,
|
priority,
|
||||||
// date,
|
date,
|
||||||
// model,
|
model,
|
||||||
// language
|
historyLength: history ? history.length : 0
|
||||||
// });
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aiAssistant = require('./ai-assistant');
|
const aiAssistant = require('./ai-assistant');
|
||||||
|
|
||||||
// Формируем промпт для LLM
|
// Создаем контекст беседы с RAG данными
|
||||||
let prompt = userQuestion;
|
const conversationContext = createConversationContext({
|
||||||
|
userQuestion,
|
||||||
|
ragAnswer: answer,
|
||||||
|
ragContext: context,
|
||||||
|
history,
|
||||||
|
product,
|
||||||
|
priority,
|
||||||
|
date
|
||||||
|
});
|
||||||
|
|
||||||
if (context) {
|
// Формируем улучшенный промпт для LLM с учетом найденной информации
|
||||||
prompt += `\n\nКонтекст: ${context}`;
|
let prompt = `Вопрос пользователя: ${userQuestion}`;
|
||||||
|
|
||||||
|
// Добавляем найденную информацию из RAG
|
||||||
|
if (answer) {
|
||||||
|
prompt += `\n\nНайденный ответ из базы знаний: ${answer}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
if (context) {
|
||||||
prompt += `\n\nНайденный ответ: ${answer}`;
|
prompt += `\n\nДополнительный контекст: ${context}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (product) {
|
if (product) {
|
||||||
prompt += `\n\nПродукт: ${product}`;
|
prompt += `\n\nПродукт: ${product}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (priority) {
|
||||||
|
prompt += `\n\nПриоритет: ${priority}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
prompt += `\n\nДата: ${date}`;
|
||||||
|
}
|
||||||
|
|
||||||
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
||||||
let finalSystemPrompt = systemPrompt;
|
let finalSystemPrompt = systemPrompt;
|
||||||
if (systemPrompt && systemPrompt.includes('{')) {
|
if (systemPrompt && systemPrompt.includes('{')) {
|
||||||
const placeholders = await getAllPlaceholdersWithValues();
|
const placeholders = await getAllPlaceholdersWithValues();
|
||||||
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
||||||
|
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
|
||||||
}
|
}
|
||||||
// --- КОНЕЦ ДОБАВЛЕНИЯ ---
|
// --- КОНЕЦ ДОБАВЛЕНИЯ ---
|
||||||
|
|
||||||
// Получаем ответ от AI
|
// Используем системный промпт из настроек, если он есть
|
||||||
const llmResponse = await aiAssistant.getResponse(
|
if (finalSystemPrompt && finalSystemPrompt.trim()) {
|
||||||
|
prompt += `\n\nСистемная инструкция: ${finalSystemPrompt}`;
|
||||||
|
} else {
|
||||||
|
// Fallback инструкция, если системный промпт не настроен
|
||||||
|
prompt += `\n\nИнструкция: Используй найденную информацию из базы знаний для ответа. Если найденный ответ подходит к вопросу пользователя, используй его как основу. Если нужно дополнить или уточнить ответ, сделай это. Поддерживай естественную беседу, учитывая предыдущие сообщения. Отвечай на русском языке кратко и по делу. Если пользователь задает уточняющие вопросы, используй контекст предыдущих ответов.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[RAG] Сформированный промпт:`, prompt.substring(0, 200) + '...');
|
||||||
|
|
||||||
|
// Получаем ответ от AI с учетом истории беседы
|
||||||
|
let llmResponse;
|
||||||
|
try {
|
||||||
|
llmResponse = await aiAssistant.getResponse(
|
||||||
prompt,
|
prompt,
|
||||||
language || 'auto',
|
|
||||||
history,
|
history,
|
||||||
finalSystemPrompt,
|
finalSystemPrompt,
|
||||||
rules
|
rules
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log(`[RAG] LLM response generated:`, llmResponse);
|
|
||||||
return llmResponse;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error(`[RAG] Error generating LLM response:`, 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 'Извините, произошла ошибка при генерации ответа.';
|
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);
|
||||||
|
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 = {
|
module.exports = {
|
||||||
ragAnswer,
|
ragAnswer,
|
||||||
getTableData,
|
getTableData,
|
||||||
generateLLMResponse
|
generateLLMResponse,
|
||||||
|
ragAnswerWithConversation
|
||||||
};
|
};
|
||||||
@@ -428,7 +428,7 @@ async function getBot() {
|
|||||||
if (ragTableId) {
|
if (ragTableId) {
|
||||||
// Сначала ищем ответ через RAG
|
// Сначала ищем ответ через RAG
|
||||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
|
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;
|
aiResponse = ragResult.answer;
|
||||||
} else {
|
} else {
|
||||||
aiResponse = await generateLLMResponse({
|
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 = {
|
module.exports = {
|
||||||
initWSS,
|
initWSS,
|
||||||
broadcastContactsUpdate,
|
broadcastContactsUpdate,
|
||||||
@@ -378,6 +415,9 @@ module.exports = {
|
|||||||
broadcastProposalCreated,
|
broadcastProposalCreated,
|
||||||
broadcastProposalVoted,
|
broadcastProposalVoted,
|
||||||
broadcastProposalExecuted,
|
broadcastProposalExecuted,
|
||||||
|
broadcastAuthTokenAdded,
|
||||||
|
broadcastAuthTokenDeleted,
|
||||||
|
broadcastAuthTokenUpdated,
|
||||||
getConnectedUsers,
|
getConnectedUsers,
|
||||||
getStats
|
getStats
|
||||||
};
|
};
|
||||||
@@ -45,14 +45,16 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: '3.5'
|
cpus: '4.0'
|
||||||
memory: 12G
|
memory: 16G
|
||||||
reservations:
|
reservations:
|
||||||
cpus: '2.0'
|
cpus: '3.0'
|
||||||
memory: 6G
|
memory: 12G
|
||||||
environment:
|
environment:
|
||||||
- OLLAMA_HOST=0.0.0.0
|
- OLLAMA_HOST=0.0.0.0
|
||||||
- OLLAMA_ORIGINS=*
|
- OLLAMA_ORIGINS=*
|
||||||
|
- OLLAMA_NUM_PARALLEL=1
|
||||||
|
- OLLAMA_NUM_GPU=1
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "ollama", "list"]
|
test: ["CMD", "ollama", "list"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
108
docs/MODULE_ARCHITECTURE.md
Normal file
108
docs/MODULE_ARCHITECTURE.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Архитектура модулей DLE
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
DLE использует модульную архитектуру, где каждый модуль может иметь свои правила доступа и функциональность.
|
||||||
|
|
||||||
|
## Типы модулей
|
||||||
|
|
||||||
|
### 1. Простые модули (Вариант 1)
|
||||||
|
Модули сами проверяют права доступа токен-холдеров.
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
contract SimpleModule {
|
||||||
|
address public dleContract;
|
||||||
|
|
||||||
|
modifier onlyDLEHolders() {
|
||||||
|
require(IERC20(dleContract).balanceOf(msg.sender) > 0, "Must hold DLE tokens");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function someFunction() external onlyDLEHolders {
|
||||||
|
// Логика функции
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Сложные модули (Вариант 3)
|
||||||
|
Модули работают через основной контракт DLE.
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
contract ComplexModule {
|
||||||
|
address public dleContract;
|
||||||
|
|
||||||
|
function executeOperation(address caller, bytes calldata operation) external {
|
||||||
|
require(msg.sender == dleContract, "Only DLE can call");
|
||||||
|
require(IERC20(dleContract).balanceOf(caller) > 0, "Must hold tokens");
|
||||||
|
|
||||||
|
// Выполняем операцию
|
||||||
|
_executeOperation(caller, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Рекомендации по выбору
|
||||||
|
|
||||||
|
### Используйте Вариант 1 для:
|
||||||
|
- ✅ Простых операций (чтение данных)
|
||||||
|
- ✅ Модулей с минимальной логикой
|
||||||
|
- ✅ Быстрых операций
|
||||||
|
|
||||||
|
### Используйте Вариант 3 для:
|
||||||
|
- ✅ Сложных финансовых операций
|
||||||
|
- ✅ Модулей с критической логикой
|
||||||
|
- ✅ Операций, требующих аудита
|
||||||
|
|
||||||
|
## Примеры модулей
|
||||||
|
|
||||||
|
### TreasuryModule (Казна)
|
||||||
|
```solidity
|
||||||
|
contract TreasuryModule {
|
||||||
|
address public dleContract;
|
||||||
|
mapping(address => bool) public supportedTokens;
|
||||||
|
|
||||||
|
modifier onlyDLEHolders() {
|
||||||
|
require(IERC20(dleContract).balanceOf(msg.sender) > 0, "Must hold DLE tokens");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function depositToken(address token, uint256 amount) external onlyDLEHolders {
|
||||||
|
require(supportedTokens[token], "Token not supported");
|
||||||
|
IERC20(token).transferFrom(msg.sender, address(this), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withdrawToken(address token, uint256 amount) external onlyDLEHolders {
|
||||||
|
require(supportedTokens[token], "Token not supported");
|
||||||
|
IERC20(token).transfer(msg.sender, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GovernanceModule (Управление)
|
||||||
|
```solidity
|
||||||
|
contract GovernanceModule {
|
||||||
|
address public dleContract;
|
||||||
|
|
||||||
|
function executeOperation(address caller, bytes calldata operation) external {
|
||||||
|
require(msg.sender == dleContract, "Only DLE can call");
|
||||||
|
require(IERC20(dleContract).balanceOf(caller) > 0, "Must hold tokens");
|
||||||
|
|
||||||
|
// Выполняем операцию управления
|
||||||
|
_executeGovernanceOperation(caller, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Общие принципы:
|
||||||
|
1. **Всегда проверяйте** баланс токенов DLE
|
||||||
|
2. **Валидируйте входные данные** в модулях
|
||||||
|
3. **Используйте ReentrancyGuard** для финансовых операций
|
||||||
|
4. **Логируйте важные операции** через события
|
||||||
|
|
||||||
|
### Аудит модулей:
|
||||||
|
- Проверяйте права доступа
|
||||||
|
- Тестируйте граничные случаи
|
||||||
|
- Валидируйте входные параметры
|
||||||
|
- Проверяйте обработку ошибок
|
||||||
@@ -50,6 +50,176 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Многоагентная архитектура AI-ассистента
|
||||||
|
|
||||||
|
### 🎯 Главный AI-координатор
|
||||||
|
- **Роль:** Анализирует входящие сообщения и координирует работу специализированных агентов
|
||||||
|
- **Функции:**
|
||||||
|
- Определяет какие агенты нужны для обработки сообщения
|
||||||
|
- Собирает результаты от всех агентов
|
||||||
|
- Генерирует финальный персонализированный ответ
|
||||||
|
- Управляет контекстом беседы
|
||||||
|
|
||||||
|
### 🤖 Специализированные агенты
|
||||||
|
|
||||||
|
#### 1. Агент "Персонализация пользователя"
|
||||||
|
- **Задача:** Извлечение и управление персональными данными
|
||||||
|
- **Функции:**
|
||||||
|
- Извлекает имя из сообщений ("меня зовут Саша")
|
||||||
|
- Анализирует профиль пользователя (компания, должность, предпочтения)
|
||||||
|
- Отслеживает историю взаимодействий
|
||||||
|
- Определяет стадию в воронке продаж
|
||||||
|
- **Результат:** Персонализированный контекст для ответа
|
||||||
|
|
||||||
|
#### 2. Агент "Анализ запроса"
|
||||||
|
- **Задача:** Классификация и понимание сути обращения
|
||||||
|
- **Функции:**
|
||||||
|
- Определяет тип вопроса (техническая проблема, вопрос о цене, жалоба)
|
||||||
|
- Анализирует эмоциональное состояние клиента
|
||||||
|
- Выявляет скрытые потребности
|
||||||
|
- Определяет приоритетность запроса
|
||||||
|
- **Результат:** Структурированный анализ запроса
|
||||||
|
|
||||||
|
#### 3. Агент "RAG поиск"
|
||||||
|
- **Задача:** Поиск релевантной информации в базе знаний
|
||||||
|
- **Функции:**
|
||||||
|
- Векторный поиск по RAG базе
|
||||||
|
- Фильтрация по тегам пользователя
|
||||||
|
- Поиск похожих случаев и решений
|
||||||
|
- Извлечение контекстной информации
|
||||||
|
- **Результат:** Релевантные ответы и шаблоны
|
||||||
|
|
||||||
|
#### 4. Агент "Контекст беседы"
|
||||||
|
- **Задача:** Анализ истории взаимодействий
|
||||||
|
- **Функции:**
|
||||||
|
- Изучает предыдущие сообщения в беседе
|
||||||
|
- Анализирует все предыдущие обращения пользователя
|
||||||
|
- Определяет повторяющиеся темы и проблемы
|
||||||
|
- Отслеживает прогресс в решении задач
|
||||||
|
- **Результат:** Контекстная картина взаимодействия
|
||||||
|
|
||||||
|
#### 5. Агент "Детализация"
|
||||||
|
- **Задача:** Выяснение недостающей информации
|
||||||
|
- **Функции:**
|
||||||
|
- Формулирует уточняющие вопросы
|
||||||
|
- Определяет какие детали нужны для решения
|
||||||
|
- Адаптирует вопросы под контекст беседы
|
||||||
|
- Отслеживает ответы на уточняющие вопросы
|
||||||
|
- **Результат:** Структурированные уточняющие вопросы
|
||||||
|
|
||||||
|
#### 6. Агент "Персонализация ответа"
|
||||||
|
- **Задача:** Адаптация ответа под конкретного пользователя
|
||||||
|
- **Функции:**
|
||||||
|
- Учитывает стиль общения пользователя
|
||||||
|
- Адаптирует тон (формальный/неформальный)
|
||||||
|
- Использует имя и персональные данные
|
||||||
|
- Ссылается на предыдущие взаимодействия
|
||||||
|
- **Результат:** Персонализированный ответ
|
||||||
|
|
||||||
|
#### 7. Агент "Мультиязычность"
|
||||||
|
- **Задача:** Обработка многоязычных запросов
|
||||||
|
- **Функции:**
|
||||||
|
- Определяет язык входящего сообщения
|
||||||
|
- Ищет ответы на соответствующем языке
|
||||||
|
- Генерирует ответы на языке пользователя
|
||||||
|
- Адаптирует культурные особенности
|
||||||
|
- **Результат:** Локализованный ответ
|
||||||
|
|
||||||
|
#### 8. Агент "Мультимодальность"
|
||||||
|
- **Задача:** Обработка различных типов контента
|
||||||
|
- **Функции:**
|
||||||
|
- Анализ изображений, аудио, видео
|
||||||
|
- Извлечение текста из медиафайлов
|
||||||
|
- Поиск похожих медиа в базе знаний
|
||||||
|
- Генерация мультимодальных ответов
|
||||||
|
- **Результат:** Контекст из медиафайлов
|
||||||
|
|
||||||
|
### ⚙️ Логика работы многоагентной системы
|
||||||
|
|
||||||
|
#### Шаг 1: Получение сообщения
|
||||||
|
- Координатор получает входящее сообщение
|
||||||
|
- Анализирует базовый контекст
|
||||||
|
- Определяет необходимых агентов
|
||||||
|
|
||||||
|
#### Шаг 2: Параллельный запуск агентов
|
||||||
|
- Агент "Персонализация" → извлекает данные пользователя
|
||||||
|
- Агент "Анализ запроса" → классифицирует обращение
|
||||||
|
- Агент "RAG поиск" → ищет релевантную информацию
|
||||||
|
- Агент "Контекст" → анализирует историю
|
||||||
|
- Агент "Мультиязычность" → определяет язык
|
||||||
|
- Агент "Мультимодальность" → обрабатывает медиа
|
||||||
|
|
||||||
|
#### Шаг 3: Сбор и анализ результатов
|
||||||
|
- Координатор собирает данные от всех агентов
|
||||||
|
- Анализирует полноту информации
|
||||||
|
- Определяет необходимость дополнительных уточнений
|
||||||
|
|
||||||
|
#### Шаг 4: Генерация ответа
|
||||||
|
- Если информации достаточно → генерирует персонализированный ответ
|
||||||
|
- Если нужно уточнить → запускает агента "Детализация"
|
||||||
|
- Если требуется дополнительный контекст → запрашивает у других агентов
|
||||||
|
|
||||||
|
#### Шаг 5: Сохранение контекста
|
||||||
|
- Обновляет профиль пользователя
|
||||||
|
- Сохраняет контекст беседы
|
||||||
|
- Логирует использованные знания
|
||||||
|
|
||||||
|
### 🎨 Преимущества многоагентной архитектуры
|
||||||
|
|
||||||
|
1. **Модульность:** Каждый агент решает свою специализированную задачу
|
||||||
|
2. **Масштабируемость:** Легко добавлять новых агентов
|
||||||
|
3. **Эффективность:** Параллельная обработка разных аспектов
|
||||||
|
4. **Гибкость:** Разные комбинации агентов для разных ситуаций
|
||||||
|
5. **Персонализация:** Глубокое понимание каждого пользователя
|
||||||
|
6. **Качество:** Специализированная обработка каждого аспекта
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Персонализация на уровне аккаунта пользователя
|
||||||
|
|
||||||
|
### 👤 Профиль пользователя
|
||||||
|
- **Базовые данные:** Имя, компания, должность, контактная информация
|
||||||
|
- **История взаимодействий:** Все предыдущие обращения и решения
|
||||||
|
- **Предпочтения:** Стиль общения, технический уровень, приоритеты
|
||||||
|
- **Статус:** Стадия в воронке продаж, статус клиента
|
||||||
|
- **Теги:** Категории, сегменты, специализации
|
||||||
|
|
||||||
|
### 📊 Контекстная картина
|
||||||
|
- **Текущая беседа:** Сообщения в рамках одной сессии
|
||||||
|
- **История обращений:** Все предыдущие взаимодействия
|
||||||
|
- **Решенные проблемы:** Успешно закрытые задачи
|
||||||
|
- **Открытые вопросы:** Незавершенные обращения
|
||||||
|
- **Эмоциональное состояние:** Тон и настроение клиента
|
||||||
|
|
||||||
|
### 🎯 Алгоритм персонализации
|
||||||
|
|
||||||
|
#### 1. Анализ входящего сообщения
|
||||||
|
- Определение типа обращения
|
||||||
|
- Извлечение ключевой информации
|
||||||
|
- Анализ эмоционального контекста
|
||||||
|
|
||||||
|
#### 2. Загрузка профиля пользователя
|
||||||
|
- Получение персональных данных
|
||||||
|
- Анализ истории взаимодействий
|
||||||
|
- Определение текущего статуса
|
||||||
|
|
||||||
|
#### 3. Поиск в RAG базе
|
||||||
|
- Фильтрация по тегам пользователя
|
||||||
|
- Поиск релевантных решений
|
||||||
|
- Анализ похожих случаев
|
||||||
|
|
||||||
|
#### 4. Формирование контекста
|
||||||
|
- Объединение данных профиля и истории
|
||||||
|
- Анализ текущей ситуации
|
||||||
|
- Определение оптимального подхода
|
||||||
|
|
||||||
|
#### 5. Генерация персонализированного ответа
|
||||||
|
- Учет персональных данных
|
||||||
|
- Адаптация под стиль общения
|
||||||
|
- Ссылки на предыдущие взаимодействия
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Этап 1. Проектирование и подготовка инфраструктуры
|
## Этап 1. Проектирование и подготовка инфраструктуры
|
||||||
1. **Проектирование схемы хранения знаний (RAG):**
|
1. **Проектирование схемы хранения знаний (RAG):**
|
||||||
- Описать структуру таблицы `knowledge_documents` (миграция).
|
- Описать структуру таблицы `knowledge_documents` (миграция).
|
||||||
@@ -69,7 +239,26 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 3. Интеграция RAG в pipeline ассистента
|
## Этап 3. Разработка многоагентной архитектуры
|
||||||
|
1. **Создание базовой структуры агентов:**
|
||||||
|
- Реализовать главный AI-координатор
|
||||||
|
- Создать базовые классы для специализированных агентов
|
||||||
|
- Настроить систему координации между агентами
|
||||||
|
2. **Разработка специализированных агентов:**
|
||||||
|
- Агент "Персонализация пользователя"
|
||||||
|
- Агент "Анализ запроса"
|
||||||
|
- Агент "RAG поиск"
|
||||||
|
- Агент "Контекст беседы"
|
||||||
|
- Агент "Детализация"
|
||||||
|
- Агент "Персонализация ответа"
|
||||||
|
3. **Интеграция с существующей системой:**
|
||||||
|
- Подключение агентов к текущему pipeline
|
||||||
|
- Настройка логирования и мониторинга
|
||||||
|
- Тестирование взаимодействия агентов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 4. Интеграция RAG в pipeline ассистента
|
||||||
1. **Модификация логики ответа ассистента:**
|
1. **Модификация логики ответа ассистента:**
|
||||||
- При получении сообщения пользователя — искать релевантные знания и включать их в prompt LLM.
|
- При получении сообщения пользователя — искать релевантные знания и включать их в prompt LLM.
|
||||||
- Обеспечить мультиязычность поиска и генерации ответа.
|
- Обеспечить мультиязычность поиска и генерации ответа.
|
||||||
@@ -78,7 +267,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 4. Интерфейс для админа
|
## Этап 5. Интерфейс для админа
|
||||||
1. **UI для управления знаниями:**
|
1. **UI для управления знаниями:**
|
||||||
- Добавить на фронте раздел для просмотра, добавления, редактирования и удаления знаний.
|
- Добавить на фронте раздел для просмотра, добавления, редактирования и удаления знаний.
|
||||||
2. **UI для модерации ответов ассистента:**
|
2. **UI для модерации ответов ассистента:**
|
||||||
@@ -87,7 +276,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 5. Поддержка мультимодальности и мультиязычности
|
## Этап 6. Поддержка мультимодальности и мультиязычности
|
||||||
1. **Обработка вложений (аудио, видео, картинки):**
|
1. **Обработка вложений (аудио, видео, картинки):**
|
||||||
- Решить, как хранить и индексировать такие данные (например, хранить ссылки и метаданные, а не сами файлы).
|
- Решить, как хранить и индексировать такие данные (например, хранить ссылки и метаданные, а не сами файлы).
|
||||||
2. **Мультиязычный поиск и генерация:**
|
2. **Мультиязычный поиск и генерация:**
|
||||||
@@ -95,7 +284,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Этап 6. Тестирование и оптимизация
|
## Этап 7. Тестирование и оптимизация
|
||||||
1. **Покрытие тестами ключевых сценариев (unit, интеграционные).**
|
1. **Покрытие тестами ключевых сценариев (unit, интеграционные).**
|
||||||
2. **Оптимизация скорости поиска и генерации.**
|
2. **Оптимизация скорости поиска и генерации.**
|
||||||
3. **Документация для команды.**
|
3. **Документация для команды.**
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ DLE.sol (Один контракт)
|
|||||||
├── Система голосования (проверка баланса токенов)
|
├── Система голосования (проверка баланса токенов)
|
||||||
├── Мультиподпись (через токен-холдеров)
|
├── Мультиподпись (через токен-холдеров)
|
||||||
├── Модули (добавляемые через голосование)
|
├── Модули (добавляемые через голосование)
|
||||||
└── Мультичейн синхронизация
|
├── Мультичейн синхронизация
|
||||||
|
└── Полное управление данными DLE через кворум
|
||||||
```
|
```
|
||||||
|
|
||||||
### Требования
|
### Требования
|
||||||
@@ -42,7 +43,7 @@ DLE.sol (Один контракт)
|
|||||||
- **Описание**: Процент от общего количества токенов для принятия решений
|
- **Описание**: Процент от общего количества токенов для принятия решений
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Настройка кворума при создании DLE
|
- Настройка кворума при создании DLE
|
||||||
- Изменение кворума через голосование
|
- **Изменение кворума через голосование** ✅
|
||||||
- Расчет кворума: `(totalSupply * quorumPercentage) / 100`
|
- Расчет кворума: `(totalSupply * quorumPercentage) / 100`
|
||||||
- Проверка достижения кворума для каждого решения
|
- Проверка достижения кворума для каждого решения
|
||||||
|
|
||||||
@@ -55,7 +56,21 @@ DLE.sol (Один контракт)
|
|||||||
- Выполнение предложений после достижения кворума
|
- Выполнение предложений после достижения кворума
|
||||||
- **НЕТ админских ролей - только коллективное управление**
|
- **НЕТ админских ролей - только коллективное управление**
|
||||||
|
|
||||||
#### 4. Мультиподпись через токен-холдеров
|
#### 4. Полное управление данными DLE через кворум ✅
|
||||||
|
- **Описание**: Все данные DLE можно изменить через систему голосования
|
||||||
|
- **Функции**:
|
||||||
|
- **Изменение названия DLE** через кворум
|
||||||
|
- **Изменение символа токена** через кворум
|
||||||
|
- **Изменение местонахождения** через кворум
|
||||||
|
- **Изменение координат** через кворум
|
||||||
|
- **Изменение юрисдикции** через кворум
|
||||||
|
- **Изменение ОКТМО** через кворум
|
||||||
|
- **Изменение КПП** через кворум
|
||||||
|
- **Изменение кодов ОКВЭД** через кворум
|
||||||
|
- **Изменение процента кворума** через кворум
|
||||||
|
- **Изменение текущей цепочки** через кворум
|
||||||
|
|
||||||
|
#### 5. Мультиподпись через токен-холдеров
|
||||||
- **Описание**: Система подписей для критических операций
|
- **Описание**: Система подписей для критических операций
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Подписание операций токен-холдерами
|
- Подписание операций токен-холдерами
|
||||||
@@ -63,7 +78,7 @@ DLE.sol (Один контракт)
|
|||||||
- Сбор подписей до достижения кворума
|
- Сбор подписей до достижения кворума
|
||||||
- Выполнение операций после сбора подписей
|
- Выполнение операций после сбора подписей
|
||||||
|
|
||||||
#### 5. Казначейские функции
|
#### 6. Казначейские функции
|
||||||
- **Описание**: Управление финансами DLE через голосование
|
- **Описание**: Управление финансами DLE через голосование
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Внесение токенов в казну
|
- Внесение токенов в казну
|
||||||
@@ -71,7 +86,7 @@ DLE.sol (Один контракт)
|
|||||||
- Распределение дивидендов
|
- Распределение дивидендов
|
||||||
- Бюджетирование через предложения
|
- Бюджетирование через предложения
|
||||||
|
|
||||||
#### 6. Модульная система
|
#### 7. Модульная система
|
||||||
- **Описание**: Добавление новых функций через модули
|
- **Описание**: Добавление новых функций через модули
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Добавление модулей через голосование
|
- Добавление модулей через голосование
|
||||||
@@ -79,7 +94,7 @@ DLE.sol (Один контракт)
|
|||||||
- Изоляция модулей от основного контракта
|
- Изоляция модулей от основного контракта
|
||||||
- Обновление модулей через голосование
|
- Обновление модулей через голосование
|
||||||
|
|
||||||
#### 7. Коммуникационные функции
|
#### 8. Коммуникационные функции
|
||||||
- **Описание**: Прием сообщений и звонков
|
- **Описание**: Прием сообщений и звонков
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Прием текстовых сообщений
|
- Прием текстовых сообщений
|
||||||
@@ -98,6 +113,114 @@ DLE может владеть токенами других DLE и участв
|
|||||||
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум мультиподписей** внутри **DLE A**
|
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум мультиподписей** внутри **DLE A**
|
||||||
4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
|
4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
|
||||||
|
|
||||||
|
### Новые возможности изменения данных DLE ✅
|
||||||
|
|
||||||
|
#### 1. Обновление основной информации DLE
|
||||||
|
```solidity
|
||||||
|
function _updateDLEInfo(
|
||||||
|
string memory _name,
|
||||||
|
string memory _symbol,
|
||||||
|
string memory _location,
|
||||||
|
string memory _coordinates,
|
||||||
|
uint256 _jurisdiction,
|
||||||
|
uint256 _oktmo,
|
||||||
|
string[] memory _okvedCodes,
|
||||||
|
uint256 _kpp
|
||||||
|
) internal
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Изменение процента кворума
|
||||||
|
```solidity
|
||||||
|
function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Изменение текущей цепочки
|
||||||
|
```solidity
|
||||||
|
function _updateCurrentChainId(uint256 _newChainId) internal
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. События для отслеживания изменений
|
||||||
|
```solidity
|
||||||
|
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);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Процесс изменения данных DLE
|
||||||
|
|
||||||
|
#### 1. Создание предложения
|
||||||
|
- Любой токен-холдер создает предложение об изменении данных
|
||||||
|
- Выбирает цепочку для сбора голосов
|
||||||
|
- Указывает новые значения для изменения
|
||||||
|
|
||||||
|
#### 2. Голосование
|
||||||
|
- Токен-холдеры голосуют за/против изменения
|
||||||
|
- Голосующая сила = количество токенов
|
||||||
|
- Проверка баланса при каждом голосе
|
||||||
|
|
||||||
|
#### 3. Исполнение
|
||||||
|
- При достижении кворума предложение исполняется
|
||||||
|
- Данные DLE обновляются
|
||||||
|
- Событие эмитится для отслеживания
|
||||||
|
|
||||||
|
#### 4. Синхронизация
|
||||||
|
- Изменения синхронизируются во все поддерживаемые цепочки
|
||||||
|
- Merkle proofs обеспечивают безопасность cross-chain операций
|
||||||
|
|
||||||
|
### Примеры использования
|
||||||
|
|
||||||
|
#### Изменение названия DLE
|
||||||
|
```
|
||||||
|
1. Создание предложения: "Изменить название на 'Новое DLE'"
|
||||||
|
2. Голосование в выбранной цепочке
|
||||||
|
3. При кворуме: обновление названия
|
||||||
|
4. Синхронизация во все цепочки
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Изменение кворума
|
||||||
|
```
|
||||||
|
1. Создание предложения: "Изменить кворум с 51% на 60%"
|
||||||
|
2. Голосование в выбранной цепочке
|
||||||
|
3. При кворуме: обновление процента кворума
|
||||||
|
4. Синхронизация во все цепочки
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Изменение текущей цепочки
|
||||||
|
```
|
||||||
|
1. Создание предложения: "Изменить текущую цепочку на Polygon"
|
||||||
|
2. Голосование в выбранной цепочке
|
||||||
|
3. При кворуме: обновление currentChainId
|
||||||
|
4. Синхронизация во все цепочки
|
||||||
|
```
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
|
||||||
|
#### Валидация данных
|
||||||
|
- Проверка корректности всех входящих данных
|
||||||
|
- Валидация адресов и числовых значений
|
||||||
|
- Проверка поддержки цепочек перед изменением
|
||||||
|
|
||||||
|
#### Защита от злоупотреблений
|
||||||
|
- Все изменения только через кворум
|
||||||
|
- Проверка баланса токенов при голосовании
|
||||||
|
- Merkle proofs для cross-chain безопасности
|
||||||
|
|
||||||
|
#### Аудит изменений
|
||||||
|
- Все изменения логируются в событиях
|
||||||
|
- Возможность отслеживания истории изменений
|
||||||
|
- Прозрачность всех операций
|
||||||
|
|
||||||
|
### Иерархическая система голосования DLE
|
||||||
|
|
||||||
|
#### Концепция
|
||||||
|
DLE может владеть токенами других DLE и участвовать в их голосовании через систему кворума подписей.
|
||||||
|
|
||||||
|
#### Механизм работы
|
||||||
|
1. **DLE A** владеет токенами **DLE B**
|
||||||
|
2. **Голос DLE A** в **DLE B** прямо пропорционален количеству токенов **DLE B** на балансе **DLE A**
|
||||||
|
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум мультиподписей** внутри **DLE A**
|
||||||
|
4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
|
||||||
|
|
||||||
#### Пример
|
#### Пример
|
||||||
- **DLE A** владеет **10% токенов DLE B**
|
- **DLE A** владеет **10% токенов DLE B**
|
||||||
- Кворум в **DLE B** = **51%**
|
- Кворум в **DLE B** = **51%**
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Финальная безопасная конфигурация nginx
|
# Финальная безопасная конфигурация nginx
|
||||||
|
|
||||||
|
# Включаем WAF конфигурацию
|
||||||
|
# include /etc/nginx/conf.d/waf.conf;
|
||||||
|
|
||||||
|
# Блокировка всех подозрительных поддоменов
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Возвращаем 444 (Connection Closed Without Response) для всех неизвестных доменов
|
||||||
|
return 444;
|
||||||
|
|
||||||
|
# Логируем попытки доступа к подозрительным доменам
|
||||||
|
access_log /var/log/nginx/suspicious_domains.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Основной сервер только для легитимных доменов
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name hb3-accelerator.com www.hb3-accelerator.com localhost 127.0.0.1;
|
server_name hb3-accelerator.com www.hb3-accelerator.com localhost 127.0.0.1;
|
||||||
@@ -7,22 +23,40 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
# Блокировка по WAF правилам
|
||||||
|
# if ($bad_ip = 1) {
|
||||||
|
# return 403;
|
||||||
|
# }
|
||||||
|
|
||||||
|
# if ($bad_bot = 1) {
|
||||||
|
# return 403;
|
||||||
|
# }
|
||||||
|
|
||||||
|
# if ($bad_request = 1) {
|
||||||
|
# return 404;
|
||||||
|
# }
|
||||||
|
|
||||||
|
# if ($bad_domain = 1) {
|
||||||
|
# return 404;
|
||||||
|
# }
|
||||||
|
|
||||||
# Блокировка агрессивных сканеров
|
# Блокировка агрессивных сканеров
|
||||||
if ($http_user_agent ~* (sqlmap|nikto|dirb|gobuster|wfuzz|burp|zap|nessus|openvas)) {
|
if ($http_user_agent ~* (sqlmap|nikto|dirb|gobuster|wfuzz|burp|zap|nessus|openvas)) {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Блокировка старых браузеров и подозрительных User-Agent
|
# Блокировка только очень старых браузеров (до Chrome 50)
|
||||||
if ($http_user_agent ~* "Chrome/[1-7][0-9]\.") {
|
if ($http_user_agent ~* "Chrome/[1-4][0-9]\.") {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($http_user_agent ~* "Safari/[1-5][0-9][0-9]\.") {
|
# Блокировка только очень старых Safari (до версии 500)
|
||||||
|
if ($http_user_agent ~* "Safari/[1-4][0-9][0-9]\.") {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Блокировка подозрительных поддоменов
|
# Дополнительная проверка подозрительных поддоменов
|
||||||
if ($host !~* "^(hb3-accelerator\.com|www\.hb3-accelerator\.com|localhost|127\.0\.0\.1)$") {
|
if ($host ~* "^(test|dev|staging|admin|beta|demo|old|new|backup|www2|www3|www4|www5|www6|www7|www8|www9|www10)\.hb3-accelerator\.com$") {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +85,6 @@ server {
|
|||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Блокировка HEAD запросов к подозрительным файлам
|
|
||||||
if ($request_method = "HEAD" && $request_uri ~* "(backup|backups|bak|old|restore|\.tar|\.gz|\.sql|config\.js|sftp-config\.json)") {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Блокировка всех запросов к конфигурационным файлам
|
# Блокировка всех запросов к конфигурационным файлам
|
||||||
if ($request_uri ~* "(config\.js|sftp-config\.json|\.config\.|\.conf\.|\.ini\.|\.env\.|\.json\.)") {
|
if ($request_uri ~* "(config\.js|sftp-config\.json|\.config\.|\.conf\.|\.ini\.|\.env\.|\.json\.)") {
|
||||||
return 404;
|
return 404;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ map $http_user_agent $bad_bot {
|
|||||||
geo $bad_ip {
|
geo $bad_ip {
|
||||||
default 0;
|
default 0;
|
||||||
198.55.98.76 1;
|
198.55.98.76 1;
|
||||||
|
# Дополнительные IP будут добавляться автоматически мониторингом
|
||||||
}
|
}
|
||||||
|
|
||||||
# Блокировка подозрительных запросов
|
# Блокировка подозрительных запросов
|
||||||
@@ -57,3 +58,16 @@ map $request_uri $bad_request {
|
|||||||
~*\.(env|config|ini|conf|cfg|yml|yaml|json|xml|sql|db|bak|backup|old|tmp|temp|log)$ 1;
|
~*\.(env|config|ini|conf|cfg|yml|yaml|json|xml|sql|db|bak|backup|old|tmp|temp|log)$ 1;
|
||||||
~*(\.\.|\.\./|\.\.\\|\.\.%2f|\.\.%5c) 1;
|
~*(\.\.|\.\./|\.\.\\|\.\.%2f|\.\.%5c) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Блокировка подозрительных доменов
|
||||||
|
map $host $bad_domain {
|
||||||
|
default 0;
|
||||||
|
~*^(test|dev|staging|admin|beta|demo|old|new|backup|www2|www3|www4|www5|www6|www7|www8|www9|www10)\.hb3-accelerator\.com$ 1;
|
||||||
|
~*akamai-inputs- 1;
|
||||||
|
~*gosipgambar 1;
|
||||||
|
~*gitlab\.cloud 1;
|
||||||
|
~*autodiscover\.home 1;
|
||||||
|
~*akamai-san 1;
|
||||||
|
~*bestcupcakerecipes 1;
|
||||||
|
~*usmc1 1;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
COPY dist/ /usr/share/nginx/html/
|
COPY dist/ /usr/share/nginx/html/
|
||||||
COPY nginx-tunnel.conf /etc/nginx/conf.d/default.conf
|
COPY nginx-tunnel.conf /etc/nginx/conf.d/default.conf
|
||||||
|
# COPY nginx-waf.conf /etc/nginx/conf.d/waf.conf
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
import { useAuth, provideAuth } from './composables/useAuth';
|
import { useAuth, provideAuth } from './composables/useAuth';
|
||||||
import { fetchTokenBalances } from './services/tokens';
|
import { fetchTokenBalances } from './services/tokens';
|
||||||
import eventBus from './utils/eventBus';
|
import eventBus from './utils/eventBus';
|
||||||
|
import wsClient from './utils/websocket';
|
||||||
|
|
||||||
// Импорт стилей
|
// Импорт стилей
|
||||||
import './assets/styles/variables.css';
|
import './assets/styles/variables.css';
|
||||||
@@ -163,6 +164,28 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Подписываемся на WebSocket события для токенов
|
||||||
|
wsClient.onAuthTokenAdded(() => {
|
||||||
|
console.log('[App] WebSocket: токен добавлен, обновляем балансы');
|
||||||
|
if (auth.isAuthenticated.value) {
|
||||||
|
refreshTokenBalances();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.onAuthTokenDeleted(() => {
|
||||||
|
console.log('[App] WebSocket: токен удален, обновляем балансы');
|
||||||
|
if (auth.isAuthenticated.value) {
|
||||||
|
refreshTokenBalances();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.onAuthTokenUpdated(() => {
|
||||||
|
console.log('[App] WebSocket: токен обновлен, обновляем балансы');
|
||||||
|
if (auth.isAuthenticated.value) {
|
||||||
|
refreshTokenBalances();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Отписываемся при размонтировании компонента
|
// Отписываемся при размонтировании компонента
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
|
|||||||
@@ -205,51 +205,63 @@ export async function executeProposal(dleAddress, proposalId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Добавить модуль
|
* Создать предложение о добавлении модуля
|
||||||
* @param {string} dleAddress - Адрес DLE контракта
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} description - Описание предложения
|
||||||
|
* @param {number} duration - Длительность голосования в секундах
|
||||||
* @param {string} moduleId - ID модуля
|
* @param {string} moduleId - ID модуля
|
||||||
* @param {string} moduleAddress - Адрес модуля
|
* @param {string} moduleAddress - Адрес модуля
|
||||||
* @returns {Promise<Object>} - Результат добавления
|
* @param {number} chainId - ID цепочки для голосования
|
||||||
|
* @returns {Promise<Object>} - Результат создания предложения
|
||||||
*/
|
*/
|
||||||
export async function addModule(dleAddress, moduleId, moduleAddress) {
|
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/add-module', {
|
const response = await axios.post('/blockchain/create-add-module-proposal', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
|
description: description,
|
||||||
|
duration: duration,
|
||||||
moduleId: moduleId,
|
moduleId: moduleId,
|
||||||
moduleAddress: moduleAddress
|
moduleAddress: moduleAddress,
|
||||||
|
chainId: chainId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || 'Не удалось добавить модуль');
|
throw new Error(response.data.message || 'Не удалось создать предложение о добавлении модуля');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка добавления модуля:', error);
|
console.error('Ошибка создания предложения о добавлении модуля:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удалить модуль
|
* Создать предложение об удалении модуля
|
||||||
* @param {string} dleAddress - Адрес DLE контракта
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} description - Описание предложения
|
||||||
|
* @param {number} duration - Длительность голосования в секундах
|
||||||
* @param {string} moduleId - ID модуля
|
* @param {string} moduleId - ID модуля
|
||||||
* @returns {Promise<Object>} - Результат удаления
|
* @param {number} chainId - ID цепочки для голосования
|
||||||
|
* @returns {Promise<Object>} - Результат создания предложения
|
||||||
*/
|
*/
|
||||||
export async function removeModule(dleAddress, moduleId) {
|
export async function createRemoveModuleProposal(dleAddress, description, duration, moduleId, chainId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/remove-module', {
|
const response = await axios.post('/blockchain/create-remove-module-proposal', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
moduleId: moduleId
|
description: description,
|
||||||
|
duration: duration,
|
||||||
|
moduleId: moduleId,
|
||||||
|
chainId: chainId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || 'Не удалось удалить модуль');
|
throw new Error(response.data.message || 'Не удалось создать предложение об удалении модуля');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка удаления модуля:', error);
|
console.error('Ошибка создания предложения об удалении модуля:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -465,3 +477,238 @@ export async function getSupportedChains(dleAddress) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Деактивировать DLE (только при достижении кворума)
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} userAddress - Адрес пользователя
|
||||||
|
* @returns {Promise<Object>} - Результат деактивации
|
||||||
|
*/
|
||||||
|
export async function deactivateDLE(dleAddress, userAddress) {
|
||||||
|
try {
|
||||||
|
// Проверяем наличие браузерного кошелька
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('Браузерный кошелек не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрашиваем подключение к кошельку
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
|
// Проверяем, что подключенный адрес совпадает с userAddress
|
||||||
|
const connectedAddress = await signer.getAddress();
|
||||||
|
if (connectedAddress.toLowerCase() !== userAddress.toLowerCase()) {
|
||||||
|
throw new Error('Подключенный кошелек не совпадает с адресом пользователя');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ABI для деактивации DLE
|
||||||
|
const dleAbi = [
|
||||||
|
"function deactivate() external",
|
||||||
|
"function balanceOf(address) external view returns (uint256)",
|
||||||
|
"function totalSupply() external view returns (uint256)",
|
||||||
|
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)",
|
||||||
|
"function voteDeactivation(uint256 _proposalId, bool _support) external",
|
||||||
|
"function checkDeactivationProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached)",
|
||||||
|
"function executeDeactivationProposal(uint256 _proposalId) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
// Проверяем, что пользователь имеет токены
|
||||||
|
const balance = await dle.balanceOf(userAddress);
|
||||||
|
if (balance <= 0) {
|
||||||
|
throw new Error('Для деактивации DLE необходимо иметь токены');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что DLE не пустой (есть токены)
|
||||||
|
const totalSupply = await dle.totalSupply();
|
||||||
|
if (totalSupply <= 0) {
|
||||||
|
throw new Error('DLE не имеет токенов');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем деактивацию (функция проверит наличие валидного предложения с кворумом)
|
||||||
|
const tx = await dle.deactivate();
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
console.log('DLE деактивирован, tx hash:', tx.hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
message: 'DLE успешно деактивирован'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка деактивации DLE:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать предложение о деактивации DLE
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} description - Описание предложения
|
||||||
|
* @param {number} duration - Длительность голосования в секундах
|
||||||
|
* @param {number} chainId - ID цепочки для деактивации
|
||||||
|
* @returns {Promise<Object>} - Результат создания предложения
|
||||||
|
*/
|
||||||
|
export async function createDeactivationProposal(dleAddress, description, duration, chainId) {
|
||||||
|
try {
|
||||||
|
// Проверяем наличие браузерного кошелька
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('Браузерный кошелек не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
|
const dleAbi = [
|
||||||
|
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
const tx = await dle.createDeactivationProposal(description, duration, chainId);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
console.log('Предложение о деактивации создано, tx hash:', tx.hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
message: 'Предложение о деактивации создано'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка создания предложения о деактивации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Голосовать за предложение деактивации
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {boolean} support - Поддержка предложения
|
||||||
|
* @returns {Promise<Object>} - Результат голосования
|
||||||
|
*/
|
||||||
|
export async function voteDeactivationProposal(dleAddress, proposalId, support) {
|
||||||
|
try {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('Браузерный кошелек не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
|
const dleAbi = [
|
||||||
|
"function voteDeactivation(uint256 _proposalId, bool _support) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
const tx = await dle.voteDeactivation(proposalId, support);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
console.log('Голосование за предложение деактивации, tx hash:', tx.hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
message: `Голосование ${support ? 'за' : 'против'} предложения деактивации`
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка голосования за предложение деактивации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить результат предложения деактивации
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @returns {Promise<Object>} - Результат проверки
|
||||||
|
*/
|
||||||
|
export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/api/blockchain/check-deactivation-proposal-result', {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось проверить результат предложения деактивации');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки результата предложения деактивации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнить предложение деактивации
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
|
*/
|
||||||
|
export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||||
|
try {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('Браузерный кошелек не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
|
const dleAbi = [
|
||||||
|
"function executeDeactivationProposal(uint256 _proposalId) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
const tx = await dle.executeDeactivationProposal(proposalId);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
console.log('Предложение деактивации исполнено, tx hash:', tx.hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
message: 'Предложение деактивации успешно исполнено'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка исполнения предложения деактивации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить предложения деактивации
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Array>} - Список предложений деактивации
|
||||||
|
*/
|
||||||
|
export async function loadDeactivationProposals(dleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/api/blockchain/load-deactivation-proposals', {
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.proposals;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось загрузить предложения деактивации');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки предложений деактивации:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,6 +124,19 @@ class WebSocketClient {
|
|||||||
dleAddress: dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обработчики для токенов аутентификации
|
||||||
|
onAuthTokenAdded(callback) {
|
||||||
|
this.on('auth_token_added', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthTokenDeleted(callback) {
|
||||||
|
this.on('auth_token_deleted', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthTokenUpdated(callback) {
|
||||||
|
this.on('auth_token_updated', callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем глобальный экземпляр WebSocket клиента
|
// Создаем глобальный экземпляр WebSocket клиента
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import { reactive } from 'vue';
|
|||||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
import { useAuthContext } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
|
import eventBus from '@/utils/eventBus';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
authTokens: { type: Array, required: true }
|
authTokens: { type: Array, required: true }
|
||||||
});
|
});
|
||||||
@@ -97,7 +98,7 @@ const emit = defineEmits(['update']);
|
|||||||
const newToken = reactive({ name: '', address: '', network: '', minBalance: 0 });
|
const newToken = reactive({ name: '', address: '', network: '', minBalance: 0 });
|
||||||
|
|
||||||
const { networkGroups, networks } = useBlockchainNetworks();
|
const { networkGroups, networks } = useBlockchainNetworks();
|
||||||
const { isAdmin } = useAuthContext();
|
const { isAdmin, checkTokenBalances, address, checkAuth } = useAuthContext();
|
||||||
|
|
||||||
async function addToken() {
|
async function addToken() {
|
||||||
if (!newToken.name || !newToken.address || !newToken.network) {
|
if (!newToken.name || !newToken.address || !newToken.network) {
|
||||||
@@ -109,7 +110,30 @@ async function addToken() {
|
|||||||
...newToken,
|
...newToken,
|
||||||
minBalance: Number(newToken.minBalance) || 0
|
minBalance: Number(newToken.minBalance) || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// После добавления токена перепроверяем баланс пользователя и обновляем состояние аутентификации
|
||||||
|
try {
|
||||||
|
if (address.value) {
|
||||||
|
await checkTokenBalances(address.value);
|
||||||
|
console.log('[AuthTokensSettings] Баланс токенов перепроверен после добавления');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем состояние аутентификации чтобы отразить изменения роли
|
||||||
|
await checkAuth();
|
||||||
|
console.log('[AuthTokensSettings] Состояние аутентификации обновлено после добавления токена');
|
||||||
|
|
||||||
|
// Уведомляем App.vue об изменении настроек аутентификации
|
||||||
|
eventBus.emit('auth-settings-saved');
|
||||||
|
console.log('[AuthTokensSettings] Событие auth-settings-saved отправлено');
|
||||||
|
} catch (balanceError) {
|
||||||
|
console.error('[AuthTokensSettings] Ошибка при перепроверке баланса:', balanceError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Небольшая задержка для синхронизации с backend
|
||||||
|
setTimeout(() => {
|
||||||
emit('update');
|
emit('update');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
newToken.name = '';
|
newToken.name = '';
|
||||||
newToken.address = '';
|
newToken.address = '';
|
||||||
newToken.network = '';
|
newToken.network = '';
|
||||||
@@ -130,7 +154,29 @@ async function removeToken(index) {
|
|||||||
try {
|
try {
|
||||||
const response = await api.delete(`/settings/auth-token/${token.address}/${token.network}`);
|
const response = await api.delete(`/settings/auth-token/${token.address}/${token.network}`);
|
||||||
console.log('[AuthTokensSettings] Успешное удаление:', response.data);
|
console.log('[AuthTokensSettings] Успешное удаление:', response.data);
|
||||||
|
|
||||||
|
// После удаления токена перепроверяем баланс пользователя и обновляем состояние аутентификации
|
||||||
|
try {
|
||||||
|
if (address.value) {
|
||||||
|
await checkTokenBalances(address.value);
|
||||||
|
console.log('[AuthTokensSettings] Баланс токенов перепроверен после удаления');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем состояние аутентификации чтобы отразить изменения роли
|
||||||
|
await checkAuth();
|
||||||
|
console.log('[AuthTokensSettings] Состояние аутентификации обновлено после удаления токена');
|
||||||
|
|
||||||
|
// Уведомляем App.vue об изменении настроек аутентификации
|
||||||
|
eventBus.emit('auth-settings-saved');
|
||||||
|
console.log('[AuthTokensSettings] Событие auth-settings-saved отправлено');
|
||||||
|
} catch (balanceError) {
|
||||||
|
console.error('[AuthTokensSettings] Ошибка при перепроверке баланса:', balanceError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Небольшая задержка для синхронизации с backend
|
||||||
|
setTimeout(() => {
|
||||||
emit('update');
|
emit('update');
|
||||||
|
}, 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[AuthTokensSettings] Ошибка при удалении токена:', e);
|
console.error('[AuthTokensSettings] Ошибка при удалении токена:', e);
|
||||||
console.error('[AuthTokensSettings] Response:', e.response);
|
console.error('[AuthTokensSettings] Response:', e.response);
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import AuthTokensSettings from './AuthTokensSettings.vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useAuthContext } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import NoAccessModal from '@/components/NoAccessModal.vue';
|
import NoAccessModal from '@/components/NoAccessModal.vue';
|
||||||
|
import wsClient from '@/utils/websocket';
|
||||||
|
|
||||||
// Состояние для отображения/скрытия дополнительных настроек
|
// Состояние для отображения/скрытия дополнительных настроек
|
||||||
const showRpcSettings = ref(false);
|
const showRpcSettings = ref(false);
|
||||||
@@ -234,6 +235,22 @@ const saveSecuritySettings = async () => {
|
|||||||
// Загрузка настроек при монтировании компонента
|
// Загрузка настроек при монтировании компонента
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
|
// Подписываемся на WebSocket события для обновления списка токенов
|
||||||
|
wsClient.onAuthTokenAdded(() => {
|
||||||
|
console.log('[SecuritySettingsView] WebSocket: токен добавлен, обновляем список');
|
||||||
|
loadSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.onAuthTokenDeleted(() => {
|
||||||
|
console.log('[SecuritySettingsView] WebSocket: токен удален, обновляем список');
|
||||||
|
loadSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.onAuthTokenUpdated(() => {
|
||||||
|
console.log('[SecuritySettingsView] WebSocket: токен обновлен, обновляем список');
|
||||||
|
loadSettings();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Методы для RPC ---
|
// --- Методы для RPC ---
|
||||||
|
|||||||
@@ -19,29 +19,24 @@
|
|||||||
@auth-action-completed="$emit('auth-action-completed')"
|
@auth-action-completed="$emit('auth-action-completed')"
|
||||||
>
|
>
|
||||||
<div class="dle-proposals-management">
|
<div class="dle-proposals-management">
|
||||||
<div class="proposals-header">
|
<!-- Заголовок в стиле настроек -->
|
||||||
<div class="header-info">
|
<div class="page-header">
|
||||||
<h3>🗳️ Управление предложениями</h3>
|
<div class="header-content">
|
||||||
<div v-if="selectedDle" class="dle-info">
|
<h1>Предложения DLE</h1>
|
||||||
<span class="dle-name">{{ selectedDle.name }} ({{ selectedDle.symbol }})</span>
|
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p>
|
||||||
<span class="dle-address">{{ shortenAddress(selectedDle.dleAddress) }}</span>
|
<p v-else-if="isLoadingDle">Загрузка...</p>
|
||||||
</div>
|
<p v-else>DLE не выбран</p>
|
||||||
<div v-else-if="isLoadingDle" class="loading-info">
|
|
||||||
<span>Загрузка данных DLE...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="no-dle-info">
|
|
||||||
<span>DLE не выбран</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Фильтры и управление -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<div class="controls-header">
|
||||||
|
<h3>Фильтры</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="controls-content">
|
||||||
<!-- Список предложений -->
|
<div class="filters-row">
|
||||||
<div class="proposals-list">
|
|
||||||
<div class="list-header">
|
|
||||||
<h4>📋 Список предложений</h4>
|
|
||||||
<div class="list-filters">
|
|
||||||
<select v-model="statusFilter" class="form-control">
|
<select v-model="statusFilter" class="form-control">
|
||||||
<option value="">Все статусы</option>
|
<option value="">Все статусы</option>
|
||||||
<option value="active">Активные</option>
|
<option value="active">Активные</option>
|
||||||
@@ -59,6 +54,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="filteredProposals.length === 0" class="no-proposals">
|
<div v-if="filteredProposals.length === 0" class="no-proposals">
|
||||||
<p>Предложений пока нет</p>
|
<p>Предложений пока нет</p>
|
||||||
@@ -255,6 +251,9 @@
|
|||||||
<option value="transfer">Передача токенов</option>
|
<option value="transfer">Передача токенов</option>
|
||||||
<option value="mint">Минтинг токенов</option>
|
<option value="mint">Минтинг токенов</option>
|
||||||
<option value="burn">Сжигание токенов</option>
|
<option value="burn">Сжигание токенов</option>
|
||||||
|
<option value="updateDLEInfo">Обновить данные DLE</option>
|
||||||
|
<option value="updateQuorum">Изменить кворум</option>
|
||||||
|
<option value="updateChain">Изменить текущую цепочку</option>
|
||||||
<option value="custom">Пользовательская операция</option>
|
<option value="custom">Пользовательская операция</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -351,6 +350,111 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Параметры для обновления данных DLE -->
|
||||||
|
<div v-if="newProposal.operationType === 'updateDLEInfo'" class="operation-params">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleName">Новое название DLE:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="dleName"
|
||||||
|
v-model="newProposal.operationParams.name"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Новое название"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleSymbol">Новый символ токена:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="dleSymbol"
|
||||||
|
v-model="newProposal.operationParams.symbol"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Новый символ"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleLocation">Новое местонахождение:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="dleLocation"
|
||||||
|
v-model="newProposal.operationParams.location"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Новое местонахождение"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleCoordinates">Новые координаты:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="dleCoordinates"
|
||||||
|
v-model="newProposal.operationParams.coordinates"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="44.0422736,43.062124"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleJurisdiction">Новая юрисдикция:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="dleJurisdiction"
|
||||||
|
v-model.number="newProposal.operationParams.jurisdiction"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="643"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleOktmo">Новый ОКТМО:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="dleOktmo"
|
||||||
|
v-model.number="newProposal.operationParams.oktmo"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="45000000000"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dleKpp">Новый КПП:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="dleKpp"
|
||||||
|
v-model.number="newProposal.operationParams.kpp"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="770101001"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Параметры для изменения кворума -->
|
||||||
|
<div v-if="newProposal.operationType === 'updateQuorum'" class="operation-params">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newQuorum">Новый процент кворума:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="newQuorum"
|
||||||
|
v-model.number="newProposal.operationParams.quorumPercentage"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
placeholder="51"
|
||||||
|
>
|
||||||
|
<small class="form-text text-muted">Процент от общего количества токенов (1-100%)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Параметры для изменения текущей цепочки -->
|
||||||
|
<div v-if="newProposal.operationType === 'updateChain'" class="operation-params">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newChainId">Новая текущая цепочка:</label>
|
||||||
|
<select id="newChainId" v-model="newProposal.operationParams.chainId" class="form-control">
|
||||||
|
<option value="">-- Выберите цепочку --</option>
|
||||||
|
<option v-for="chain in availableChains" :key="chain.chainId" :value="chain.chainId">
|
||||||
|
{{ chain.name }} ({{ chain.chainId }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">Выберите новую цепочку для управления DLE</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -394,7 +498,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> <!-- Закрываем div для авторизованных пользователей -->
|
</div> <!-- Закрываем div для авторизованных пользователей -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -405,6 +508,7 @@ import { useAuthContext } from '@/composables/useAuth';
|
|||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import { getDLEInfo, loadProposals, createProposal as createProposalAPI, voteForProposal as voteForProposalAPI, executeProposal as executeProposalAPI, getSupportedChains } from '../../utils/dle-contract.js';
|
import { getDLEInfo, loadProposals, createProposal as createProposalAPI, voteForProposal as voteForProposalAPI, executeProposal as executeProposalAPI, getSupportedChains } from '../../utils/dle-contract.js';
|
||||||
import wsClient from '../../utils/websocket.js';
|
import wsClient from '../../utils/websocket.js';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dleAddress: { type: String, required: false, default: null },
|
dleAddress: { type: String, required: false, default: null },
|
||||||
@@ -453,7 +557,15 @@ const newProposal = ref({
|
|||||||
to: '',
|
to: '',
|
||||||
from: '',
|
from: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
customData: ''
|
customData: '',
|
||||||
|
name: '',
|
||||||
|
symbol: '',
|
||||||
|
location: '',
|
||||||
|
coordinates: '',
|
||||||
|
jurisdiction: 0,
|
||||||
|
oktmo: 0,
|
||||||
|
kpp: 0,
|
||||||
|
chainId: ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -546,6 +658,12 @@ function validateOperationParams() {
|
|||||||
return validateAddress(params.from) && params.amount > 0;
|
return validateAddress(params.from) && params.amount > 0;
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return params.customData && params.customData.startsWith('0x') && params.customData.length >= 10;
|
return params.customData && params.customData.startsWith('0x') && params.customData.length >= 10;
|
||||||
|
case 'updateDLEInfo':
|
||||||
|
return params.name && params.symbol && params.location && params.coordinates && params.jurisdiction && params.oktmo && params.kpp;
|
||||||
|
case 'updateQuorum':
|
||||||
|
return params.quorumPercentage >= 1 && params.quorumPercentage <= 100;
|
||||||
|
case 'updateChain':
|
||||||
|
return params.chainId && params.chainId !== '';
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -580,7 +698,10 @@ function getOperationTypeName(type) {
|
|||||||
'transfer': 'Передача токенов',
|
'transfer': 'Передача токенов',
|
||||||
'mint': 'Минтинг токенов',
|
'mint': 'Минтинг токенов',
|
||||||
'burn': 'Сжигание токенов',
|
'burn': 'Сжигание токенов',
|
||||||
'custom': 'Пользовательская операция'
|
'custom': 'Пользовательская операция',
|
||||||
|
'updateDLEInfo': 'Обновить данные DLE',
|
||||||
|
'updateQuorum': 'Изменить кворум',
|
||||||
|
'updateChain': 'Изменить текущую цепочку'
|
||||||
};
|
};
|
||||||
return types[type] || 'Неизвестный тип';
|
return types[type] || 'Неизвестный тип';
|
||||||
}
|
}
|
||||||
@@ -597,6 +718,12 @@ function getOperationParamsPreview() {
|
|||||||
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
|
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return `Данные: ${params.customData.substring(0, 20)}...`;
|
return `Данные: ${params.customData.substring(0, 20)}...`;
|
||||||
|
case 'updateDLEInfo':
|
||||||
|
return `Название: ${params.name}, Символ: ${params.symbol}, Местонахождение: ${params.location}, Координаты: ${params.coordinates}, Юрисдикция: ${params.jurisdiction}, ОКТМО: ${params.oktmo}, КПП: ${params.kpp}`;
|
||||||
|
case 'updateQuorum':
|
||||||
|
return `Процент кворума: ${params.quorumPercentage}%`;
|
||||||
|
case 'updateChain':
|
||||||
|
return `Новая цепочка: ${getChainName(params.chainId) || 'Не выбрана'}`;
|
||||||
default:
|
default:
|
||||||
return 'Не указаны';
|
return 'Не указаны';
|
||||||
}
|
}
|
||||||
@@ -620,8 +747,30 @@ function getProposalStatus(proposal) {
|
|||||||
return 'executed';
|
return 'executed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, достигнут ли кворум
|
||||||
|
const quorumPercentage = getQuorumPercentage(proposal);
|
||||||
|
const requiredQuorum = getRequiredQuorum();
|
||||||
|
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
|
||||||
|
|
||||||
|
// Добавляем отладочную информацию
|
||||||
|
console.log('[getProposalStatus] Проверка предложения:', {
|
||||||
|
proposalId: proposal.id,
|
||||||
|
now,
|
||||||
|
deadline,
|
||||||
|
deadlinePassed: deadline > 0 && now >= deadline,
|
||||||
|
quorumPercentage,
|
||||||
|
requiredQuorum,
|
||||||
|
hasReachedQuorum
|
||||||
|
});
|
||||||
|
|
||||||
|
// Если кворум достигнут, предложение можно выполнить
|
||||||
|
if (hasReachedQuorum) {
|
||||||
|
return 'succeeded';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если дедлайн истек, но кворум не достигнут
|
||||||
if (deadline > 0 && now >= deadline) {
|
if (deadline > 0 && now >= deadline) {
|
||||||
return proposal.isPassed ? 'succeeded' : 'defeated';
|
return 'defeated';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'active';
|
return 'active';
|
||||||
@@ -754,7 +903,31 @@ function canSign(proposal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canExecute(proposal) {
|
function canExecute(proposal) {
|
||||||
return proposal.status === 'succeeded' && !proposal.executed;
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const deadline = proposal.deadline || 0;
|
||||||
|
|
||||||
|
// Предложение можно выполнить только если:
|
||||||
|
// 1. Дедлайн истек
|
||||||
|
// 2. Кворум достигнут
|
||||||
|
// 3. Предложение еще не выполнено
|
||||||
|
const quorumPercentage = getQuorumPercentage(proposal);
|
||||||
|
const requiredQuorum = getRequiredQuorum();
|
||||||
|
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
|
||||||
|
const deadlinePassed = deadline > 0 && now >= deadline;
|
||||||
|
|
||||||
|
// Добавляем отладочную информацию
|
||||||
|
console.log('[canExecute] Проверка предложения:', {
|
||||||
|
proposalId: proposal.id,
|
||||||
|
quorumPercentage,
|
||||||
|
requiredQuorum,
|
||||||
|
hasReachedQuorum,
|
||||||
|
deadline,
|
||||||
|
now,
|
||||||
|
deadlinePassed,
|
||||||
|
executed: proposal.executed
|
||||||
|
});
|
||||||
|
|
||||||
|
return deadlinePassed && hasReachedQuorum && !proposal.executed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSigned(proposalId) {
|
function hasSigned(proposalId) {
|
||||||
@@ -859,6 +1032,12 @@ function encodeOperation() {
|
|||||||
return encodeBurnOperation(params.from, params.amount);
|
return encodeBurnOperation(params.from, params.amount);
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return params.customData;
|
return params.customData;
|
||||||
|
case 'updateDLEInfo':
|
||||||
|
return encodeUpdateDLEInfoOperation(params.name, params.symbol, params.location, params.coordinates, params.jurisdiction, params.oktmo, params.kpp);
|
||||||
|
case 'updateQuorum':
|
||||||
|
return encodeUpdateQuorumOperation(params.quorumPercentage);
|
||||||
|
case 'updateChain':
|
||||||
|
return encodeUpdateChainOperation(params.chainId);
|
||||||
default:
|
default:
|
||||||
throw new Error('Неизвестный тип операции');
|
throw new Error('Неизвестный тип операции');
|
||||||
}
|
}
|
||||||
@@ -888,6 +1067,42 @@ function encodeBurnOperation(from, amount) {
|
|||||||
return selector + paddedAddress + paddedAmount;
|
return selector + paddedAddress + paddedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeUpdateDLEInfoOperation(name, symbol, location, coordinates, jurisdiction, oktmo, kpp) {
|
||||||
|
// Селектор для updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)
|
||||||
|
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)')).slice(0, 10);
|
||||||
|
|
||||||
|
// Кодируем параметры
|
||||||
|
const abiCoder = new ethers.AbiCoder();
|
||||||
|
const encodedData = abiCoder.encode(
|
||||||
|
['string', 'string', 'string', 'string', 'uint256', 'uint256', 'string[]', 'uint256'],
|
||||||
|
[name, symbol, location, coordinates, jurisdiction, oktmo, [], kpp] // okvedCodes пока пустой массив
|
||||||
|
);
|
||||||
|
|
||||||
|
return selector + encodedData.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeUpdateQuorumOperation(quorumPercentage) {
|
||||||
|
// Селектор для updateQuorumPercentage(uint256)
|
||||||
|
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateQuorumPercentage(uint256)')).slice(0, 10);
|
||||||
|
|
||||||
|
// Кодируем параметр
|
||||||
|
const abiCoder = new ethers.AbiCoder();
|
||||||
|
const encodedData = abiCoder.encode(['uint256'], [quorumPercentage]);
|
||||||
|
|
||||||
|
return selector + encodedData.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeUpdateChainOperation(chainId) {
|
||||||
|
// Селектор для updateCurrentChainId(uint256)
|
||||||
|
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateCurrentChainId(uint256)')).slice(0, 10);
|
||||||
|
|
||||||
|
// Кодируем параметр
|
||||||
|
const abiCoder = new ethers.AbiCoder();
|
||||||
|
const encodedData = abiCoder.encode(['uint256'], [chainId]);
|
||||||
|
|
||||||
|
return selector + encodedData.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
// Подпись предложения
|
// Подпись предложения
|
||||||
async function signProposalLocal(proposalId) {
|
async function signProposalLocal(proposalId) {
|
||||||
// Проверка прав админа для голосования
|
// Проверка прав админа для голосования
|
||||||
@@ -994,7 +1209,15 @@ function resetForm() {
|
|||||||
to: '',
|
to: '',
|
||||||
from: '',
|
from: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
customData: ''
|
customData: '',
|
||||||
|
name: '',
|
||||||
|
symbol: '',
|
||||||
|
location: '',
|
||||||
|
coordinates: '',
|
||||||
|
jurisdiction: 0,
|
||||||
|
oktmo: 0,
|
||||||
|
kpp: 0,
|
||||||
|
chainId: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1094,9 +1317,128 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dle-proposals-management {
|
.dle-proposals-management {
|
||||||
padding: 1rem;
|
padding: 20px;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Заголовок в стиле настроек */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
color: var(--color-grey-dark);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
padding: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Секция управления */
|
||||||
|
.controls-section {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-header {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-header h3 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary:hover {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Устаревшие стили для совместимости */
|
||||||
.proposals-header {
|
.proposals-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -22,291 +22,48 @@
|
|||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1>Настройки</h1>
|
<h1>Настройки DLE</h1>
|
||||||
<p>Параметры DLE и конфигурация</p>
|
<p v-if="dleInfo">{{ dleInfo.name }} ({{ dleInfo.symbol }}) - {{ dleInfo.address }}</p>
|
||||||
|
<p v-else-if="address">Загрузка...</p>
|
||||||
|
<p v-else>DLE не выбран</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
<button class="close-btn" @click="router.push('/management')">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Основные настройки -->
|
<!-- Основной контент -->
|
||||||
<div class="main-settings-section">
|
<div v-if="dleInfo" class="main-content">
|
||||||
<h2>Основные настройки</h2>
|
<!-- Удаление DLE -->
|
||||||
<form @submit.prevent="saveMainSettings" class="settings-form">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dleName">Название DLE:</label>
|
|
||||||
<input
|
|
||||||
id="dleName"
|
|
||||||
v-model="mainSettings.name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Введите название DLE"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dleSymbol">Символ токена:</label>
|
|
||||||
<input
|
|
||||||
id="dleSymbol"
|
|
||||||
v-model="mainSettings.symbol"
|
|
||||||
type="text"
|
|
||||||
placeholder="MDLE"
|
|
||||||
maxlength="10"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dleDescription">Описание:</label>
|
|
||||||
<textarea
|
|
||||||
id="dleDescription"
|
|
||||||
v-model="mainSettings.description"
|
|
||||||
placeholder="Опишите назначение и деятельность DLE..."
|
|
||||||
rows="4"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dleLocation">Местонахождение:</label>
|
|
||||||
<input
|
|
||||||
id="dleLocation"
|
|
||||||
v-model="mainSettings.location"
|
|
||||||
type="text"
|
|
||||||
placeholder="Страна, город"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dleWebsite">Веб-сайт:</label>
|
|
||||||
<input
|
|
||||||
id="dleWebsite"
|
|
||||||
v-model="mainSettings.website"
|
|
||||||
type="url"
|
|
||||||
placeholder="https://example.com"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
|
||||||
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Настройки безопасности -->
|
|
||||||
<div class="security-settings-section">
|
|
||||||
<h2>Настройки безопасности</h2>
|
|
||||||
<form @submit.prevent="saveSecuritySettings" class="settings-form">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="minQuorum">Минимальный кворум (%):</label>
|
|
||||||
<input
|
|
||||||
id="minQuorum"
|
|
||||||
v-model="securitySettings.minQuorum"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
placeholder="51"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">Минимальный процент токенов для принятия решений</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="maxProposalDuration">Максимальная длительность предложения (дни):</label>
|
|
||||||
<input
|
|
||||||
id="maxProposalDuration"
|
|
||||||
v-model="securitySettings.maxProposalDuration"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="365"
|
|
||||||
placeholder="7"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">Максимальное время жизни предложения</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="emergencyThreshold">Порог экстренных действий (%):</label>
|
|
||||||
<input
|
|
||||||
id="emergencyThreshold"
|
|
||||||
v-model="securitySettings.emergencyThreshold"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
placeholder="75"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">Процент для экстренных действий</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="timelockDelay">Задержка таймлока (часы):</label>
|
|
||||||
<input
|
|
||||||
id="timelockDelay"
|
|
||||||
v-model="securitySettings.timelockDelay"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="168"
|
|
||||||
placeholder="24"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<span class="input-hint">Минимальная задержка перед выполнением</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Дополнительные настройки:</label>
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<label class="checkbox-item">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="securitySettings.allowDelegation"
|
|
||||||
>
|
|
||||||
Разрешить делегирование голосов
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-item">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="securitySettings.requireKYC"
|
|
||||||
>
|
|
||||||
Требовать KYC для участия
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-item">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="securitySettings.autoExecute"
|
|
||||||
>
|
|
||||||
Автоматическое выполнение предложений
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
|
||||||
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки безопасности' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Настройки сети -->
|
|
||||||
<div class="network-settings-section">
|
|
||||||
<h2>Настройки сети</h2>
|
|
||||||
<form @submit.prevent="saveNetworkSettings" class="settings-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Поддерживаемые сети:</label>
|
|
||||||
<div class="networks-grid">
|
|
||||||
<label
|
|
||||||
v-for="network in availableNetworks"
|
|
||||||
:key="network.id"
|
|
||||||
class="network-checkbox"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:value="network.id"
|
|
||||||
v-model="networkSettings.enabledNetworks"
|
|
||||||
>
|
|
||||||
<div class="network-info">
|
|
||||||
<span class="network-name">{{ network.name }}</span>
|
|
||||||
<span class="network-chain-id">Chain ID: {{ network.chainId }}</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="defaultNetwork">Сеть по умолчанию:</label>
|
|
||||||
<select id="defaultNetwork" v-model="networkSettings.defaultNetwork" required>
|
|
||||||
<option value="">Выберите сеть</option>
|
|
||||||
<option
|
|
||||||
v-for="network in availableNetworks"
|
|
||||||
:key="network.id"
|
|
||||||
:value="network.id"
|
|
||||||
>
|
|
||||||
{{ network.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="rpcEndpoint">RPC Endpoint:</label>
|
|
||||||
<input
|
|
||||||
id="rpcEndpoint"
|
|
||||||
v-model="networkSettings.rpcEndpoint"
|
|
||||||
type="url"
|
|
||||||
placeholder="https://mainnet.infura.io/v3/YOUR_PROJECT_ID"
|
|
||||||
>
|
|
||||||
<span class="input-hint">RPC endpoint для подключения к блокчейну</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
|
||||||
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки сети' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Резервное копирование -->
|
|
||||||
<div class="backup-section">
|
|
||||||
<h2>Резервное копирование</h2>
|
|
||||||
<div class="backup-actions">
|
|
||||||
<div class="backup-card">
|
|
||||||
<h3>Экспорт настроек</h3>
|
|
||||||
<p>Скачайте файл с настройками DLE для резервного копирования</p>
|
|
||||||
<button @click="exportSettings" class="btn-secondary">
|
|
||||||
Экспортировать
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="backup-card">
|
|
||||||
<h3>Импорт настроек</h3>
|
|
||||||
<p>Загрузите файл с настройками для восстановления</p>
|
|
||||||
<input
|
|
||||||
ref="importFile"
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
@change="importSettings"
|
|
||||||
style="display: none"
|
|
||||||
>
|
|
||||||
<button @click="$refs.importFile.click()" class="btn-secondary">
|
|
||||||
Импортировать
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Опасная зона -->
|
|
||||||
<div class="danger-zone-section">
|
|
||||||
<h2>Опасная зона</h2>
|
|
||||||
<div class="danger-actions">
|
|
||||||
<div class="danger-card">
|
|
||||||
<h3>Сброс настроек</h3>
|
|
||||||
<p>Вернуть все настройки к значениям по умолчанию</p>
|
|
||||||
<button @click="resetSettings" class="btn-danger">
|
|
||||||
Сбросить настройки
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="danger-card">
|
<div class="danger-card">
|
||||||
|
<div class="danger-header">
|
||||||
<h3>Удаление DLE</h3>
|
<h3>Удаление DLE</h3>
|
||||||
<p>Полное удаление DLE и всех связанных данных</p>
|
</div>
|
||||||
<button @click="deleteDLE" class="btn-danger">
|
<div class="danger-content">
|
||||||
Удалить DLE
|
<p>Полное удаление DLE и всех связанных данных. Это действие необратимо.</p>
|
||||||
|
<button @click="deleteDLE" class="btn-danger" :disabled="isLoading">
|
||||||
|
{{ isLoading ? 'Загрузка...' : 'Удалить DLE' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Сообщение если DLE не выбран -->
|
||||||
|
<div v-if="!address" class="no-dle-card">
|
||||||
|
<h3>DLE не выбран</h3>
|
||||||
|
<p>Для управления настройками необходимо выбрать DLE</p>
|
||||||
|
<button @click="router.push('/management')" class="btn-primary">
|
||||||
|
Вернуться к списку DLE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits } from 'vue';
|
import { ref, defineProps, defineEmits, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
import { getDLEInfo, deactivateDLE } from '../../utils/dle-contract.js';
|
||||||
|
|
||||||
// Определяем props
|
// Определяем props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -320,196 +77,117 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['auth-action-completed']);
|
const emit = defineEmits(['auth-action-completed']);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// Состояние
|
// Состояние
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
|
const dleAddress = ref('');
|
||||||
|
const dleInfo = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// Основные настройки
|
// Получаем адрес DLE из URL параметров
|
||||||
const mainSettings = ref({
|
const address = route.query.address || props.dleAddress;
|
||||||
name: 'Мое DLE',
|
|
||||||
symbol: 'MDLE',
|
|
||||||
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
|
|
||||||
location: 'Россия, Москва',
|
|
||||||
website: 'https://example.com'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Настройки безопасности
|
// Получаем адрес пользователя из контекста аутентификации
|
||||||
const securitySettings = ref({
|
const { address: userAddress } = useAuthContext();
|
||||||
minQuorum: 51,
|
|
||||||
maxProposalDuration: 7,
|
|
||||||
emergencyThreshold: 75,
|
|
||||||
timelockDelay: 24,
|
|
||||||
allowDelegation: true,
|
|
||||||
requireKYC: false,
|
|
||||||
autoExecute: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Настройки сети
|
// Загружаем информацию о DLE
|
||||||
const networkSettings = ref({
|
const loadDLEInfo = async () => {
|
||||||
enabledNetworks: [1, 137],
|
if (!address) {
|
||||||
defaultNetwork: 1,
|
console.error('Адрес DLE не указан');
|
||||||
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
// Доступные сети (загружаются из конфигурации)
|
try {
|
||||||
const availableNetworks = ref([]);
|
isLoading.value = true;
|
||||||
|
console.log('Загружаем информацию о DLE:', address);
|
||||||
|
|
||||||
|
// Загружаем данные DLE из блокчейна через API
|
||||||
|
const dleData = await getDLEInfo(address);
|
||||||
|
console.log('Загружены данные DLE из блокчейна:', dleData);
|
||||||
|
|
||||||
|
dleInfo.value = {
|
||||||
|
name: dleData.name, // Название DLE из блокчейна
|
||||||
|
symbol: dleData.symbol, // Символ DLE из блокчейна
|
||||||
|
address: dleData.dleAddress || address // Адрес из API или из URL
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке информации о DLE:', error);
|
||||||
|
// В случае ошибки показываем базовую информацию
|
||||||
|
dleInfo.value = {
|
||||||
|
name: 'DLE ' + address.slice(0, 8) + '...',
|
||||||
|
symbol: 'DLE',
|
||||||
|
address: address
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
const saveMainSettings = async () => {
|
const deleteDLE = async () => {
|
||||||
if (isSaving.value) return;
|
if (!address) {
|
||||||
|
alert('Адрес DLE не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем аутентификацию
|
||||||
|
if (!props.isAuthenticated || !userAddress.value) {
|
||||||
|
alert('❌ Для удаления DLE необходимо авторизоваться в приложении');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`ВНИМАНИЕ! Это действие необратимо. Вы уверены, что хотите деактивировать DLE ${dleInfo.value?.name || address}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Это действие нельзя отменить. Подтвердите деактивацию еще раз.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
|
console.log('Деактивация DLE:', address);
|
||||||
|
|
||||||
// Здесь будет логика сохранения основных настроек
|
// Выполняем деактивацию DLE
|
||||||
// console.log('Сохранение основных настроек:', mainSettings.value);
|
const result = await deactivateDLE(address, userAddress.value);
|
||||||
|
|
||||||
// Временная логика
|
console.log('Результат деактивации:', result);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
alert('Основные настройки успешно сохранены!');
|
alert(`✅ DLE ${dleInfo.value?.name || address} успешно деактивирован!\n\nТранзакция: ${result.txHash}`);
|
||||||
|
|
||||||
|
// Перенаправляем на главную страницу управления
|
||||||
|
router.push('/management');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('Ошибка сохранения основных настроек:', error);
|
console.error('Ошибка при деактивации DLE:', error);
|
||||||
alert('Ошибка при сохранении настроек');
|
|
||||||
|
let errorMessage = 'Ошибка при деактивации DLE';
|
||||||
|
|
||||||
|
if (error.message.includes('владелец')) {
|
||||||
|
errorMessage = '❌ Только владелец DLE может его деактивировать';
|
||||||
|
} else if (error.message.includes('кошелек')) {
|
||||||
|
errorMessage = '❌ Необходимо подключить кошелек';
|
||||||
|
} else if (error.message.includes('деактивирован')) {
|
||||||
|
errorMessage = '❌ DLE уже деактивирован';
|
||||||
|
} else {
|
||||||
|
errorMessage = `❌ Ошибка: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveSecuritySettings = async () => {
|
// Загружаем данные при монтировании компонента
|
||||||
if (isSaving.value) return;
|
onMounted(() => {
|
||||||
|
if (address) {
|
||||||
try {
|
dleAddress.value = address;
|
||||||
isSaving.value = true;
|
loadDLEInfo();
|
||||||
|
|
||||||
// Здесь будет логика сохранения настроек безопасности
|
|
||||||
// console.log('Сохранение настроек безопасности:', securitySettings.value);
|
|
||||||
|
|
||||||
// Временная логика
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
alert('Настройки безопасности успешно сохранены!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка сохранения настроек безопасности:', error);
|
|
||||||
alert('Ошибка при сохранении настроек безопасности');
|
|
||||||
} finally {
|
|
||||||
isSaving.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const saveNetworkSettings = async () => {
|
|
||||||
if (isSaving.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isSaving.value = true;
|
|
||||||
|
|
||||||
// Здесь будет логика сохранения настроек сети
|
|
||||||
// console.log('Сохранение настроек сети:', networkSettings.value);
|
|
||||||
|
|
||||||
// Временная логика
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
alert('Настройки сети успешно сохранены!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка сохранения настроек сети:', error);
|
|
||||||
alert('Ошибка при сохранении настроек сети');
|
|
||||||
} finally {
|
|
||||||
isSaving.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportSettings = () => {
|
|
||||||
const settings = {
|
|
||||||
main: mainSettings.value,
|
|
||||||
security: securitySettings.value,
|
|
||||||
network: networkSettings.value,
|
|
||||||
exportDate: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `dle-settings-${new Date().toISOString().split('T')[0]}.json`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
alert('Настройки успешно экспортированы!');
|
|
||||||
};
|
|
||||||
|
|
||||||
const importSettings = (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const settings = JSON.parse(e.target.result);
|
|
||||||
|
|
||||||
if (settings.main) mainSettings.value = settings.main;
|
|
||||||
if (settings.security) securitySettings.value = settings.security;
|
|
||||||
if (settings.network) networkSettings.value = settings.network;
|
|
||||||
|
|
||||||
alert('Настройки успешно импортированы!');
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка импорта настроек:', error);
|
|
||||||
alert('Ошибка при импорте настроек');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSettings = () => {
|
|
||||||
if (!confirm('Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сброс к значениям по умолчанию
|
|
||||||
mainSettings.value = {
|
|
||||||
name: 'Мое DLE',
|
|
||||||
symbol: 'MDLE',
|
|
||||||
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
|
|
||||||
location: 'Россия, Москва',
|
|
||||||
website: 'https://example.com'
|
|
||||||
};
|
|
||||||
|
|
||||||
securitySettings.value = {
|
|
||||||
minQuorum: 51,
|
|
||||||
maxProposalDuration: 7,
|
|
||||||
emergencyThreshold: 75,
|
|
||||||
timelockDelay: 24,
|
|
||||||
allowDelegation: true,
|
|
||||||
requireKYC: false,
|
|
||||||
autoExecute: false
|
|
||||||
};
|
|
||||||
|
|
||||||
networkSettings.value = {
|
|
||||||
enabledNetworks: [1, 137],
|
|
||||||
defaultNetwork: 1,
|
|
||||||
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
|
|
||||||
};
|
|
||||||
|
|
||||||
alert('Настройки сброшены к значениям по умолчанию!');
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDLE = () => {
|
|
||||||
if (!confirm('ВНИМАНИЕ! Это действие необратимо. Вы уверены, что хотите удалить DLE и все связанные данные?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm('Это действие нельзя отменить. Подтвердите удаление еще раз.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Здесь будет логика удаления DLE
|
|
||||||
// console.log('Удаление DLE...');
|
|
||||||
alert('DLE будет удален. Это действие может занять некоторое время.');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -525,10 +203,10 @@ const deleteDLE = () => {
|
|||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 30px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 15px;
|
||||||
border-bottom: 2px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
@@ -537,13 +215,13 @@ const deleteDLE = () => {
|
|||||||
|
|
||||||
.page-header h1 {
|
.page-header h1 {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-size: 2.5rem;
|
font-size: 2rem;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header p {
|
.page-header p {
|
||||||
color: var(--color-grey-dark);
|
color: var(--color-grey-dark);
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,261 +247,92 @@ const deleteDLE = () => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Секции */
|
/* Основной контент */
|
||||||
.main-settings-section,
|
.main-content {
|
||||||
.security-settings-section,
|
|
||||||
.network-settings-section,
|
|
||||||
.backup-section,
|
|
||||||
.danger-zone-section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-settings-section h2,
|
|
||||||
.security-settings-section h2,
|
|
||||||
.network-settings-section h2,
|
|
||||||
.backup-section h2,
|
|
||||||
.danger-zone-section h2 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Формы настроек */
|
|
||||||
.settings-form {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select,
|
|
||||||
.form-group textarea {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-hint {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Чекбоксы */
|
|
||||||
.checkbox-group {
|
|
||||||
display: grid;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-item:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-item input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сети */
|
|
||||||
.networks-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 15px;
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-checkbox:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-checkbox input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-chain-id {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Резервное копирование */
|
|
||||||
.backup-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-card {
|
|
||||||
background: white;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-card h3 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-card p {
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Опасная зона */
|
|
||||||
.danger-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Карточки */
|
||||||
.danger-card {
|
.danger-card {
|
||||||
background: #fff5f5;
|
background: white;
|
||||||
padding: 25px;
|
border: 1px solid #e9ecef;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border: 1px solid #fed7d7;
|
overflow: hidden;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-card h3 {
|
.danger-header {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-header h3 {
|
||||||
color: #c53030;
|
color: #c53030;
|
||||||
margin-bottom: 15px;
|
margin: 0;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-card p {
|
.danger-content {
|
||||||
color: var(--color-grey-dark);
|
padding: 20px;
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Кнопки */
|
/* Кнопки */
|
||||||
|
.btn-primary,
|
||||||
|
.btn-danger {
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover {
|
||||||
background: var(--color-primary-dark);
|
background: var(--color-primary-dark);
|
||||||
}
|
transform: translateY(-1px);
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--color-secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--color-secondary-dark);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: #dc3545;
|
background: #e53e3e;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
background: #c82333;
|
background: #c53030;
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивность */
|
.btn-primary:active,
|
||||||
@media (max-width: 768px) {
|
.btn-danger:active {
|
||||||
.form-row {
|
transform: translateY(0);
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.networks-grid {
|
/* Сообщение если DLE не выбран */
|
||||||
grid-template-columns: 1fr;
|
.no-dle-card {
|
||||||
|
background: #fff5f5;
|
||||||
|
border: 2px solid #fed7d7;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backup-actions,
|
.no-dle-card h3 {
|
||||||
.danger-actions {
|
color: #c53030;
|
||||||
grid-template-columns: 1fr;
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-dle-card p {
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -4,16 +4,18 @@
|
|||||||
# Автоматически блокирует подозрительные IP адреса и домены
|
# Автоматически блокирует подозрительные IP адреса и домены
|
||||||
|
|
||||||
LOG_FILE="/var/log/nginx/access.log"
|
LOG_FILE="/var/log/nginx/access.log"
|
||||||
|
SUSPICIOUS_LOG_FILE="/var/log/nginx/suspicious_domains.log"
|
||||||
BLOCKED_IPS_FILE="/tmp/blocked_ips.txt"
|
BLOCKED_IPS_FILE="/tmp/blocked_ips.txt"
|
||||||
SUSPICIOUS_DOMAINS_FILE="/tmp/suspicious_domains.txt"
|
SUSPICIOUS_DOMAINS_FILE="/tmp/suspicious_domains.txt"
|
||||||
NGINX_CONTAINER="dapp-frontend-nginx"
|
NGINX_CONTAINER="dapp-frontend-nginx"
|
||||||
|
WAF_CONF_FILE="/etc/nginx/conf.d/waf.conf"
|
||||||
|
|
||||||
# Создаем файлы для хранения данных
|
# Создаем файлы для хранения данных
|
||||||
touch "$BLOCKED_IPS_FILE"
|
touch "$BLOCKED_IPS_FILE"
|
||||||
touch "$SUSPICIOUS_DOMAINS_FILE"
|
touch "$SUSPICIOUS_DOMAINS_FILE"
|
||||||
|
|
||||||
echo "🔒 Запуск мониторинга безопасности DLE..."
|
echo "🔒 Запуск мониторинга безопасности DLE..."
|
||||||
echo "📊 Логирование атак в: $LOG_FILE"
|
echo "📊 Анализ логов nginx контейнера: $NGINX_CONTAINER"
|
||||||
echo "🚫 Заблокированные IP: $BLOCKED_IPS_FILE"
|
echo "🚫 Заблокированные IP: $BLOCKED_IPS_FILE"
|
||||||
echo "🌐 Подозрительные домены: $SUSPICIOUS_DOMAINS_FILE"
|
echo "🌐 Подозрительные домены: $SUSPICIOUS_DOMAINS_FILE"
|
||||||
|
|
||||||
@@ -30,8 +32,31 @@ SUSPICIOUS_DOMAINS=(
|
|||||||
"akamai-inputs-rvc"
|
"akamai-inputs-rvc"
|
||||||
"akamai-inputs-erau"
|
"akamai-inputs-erau"
|
||||||
"akamai-inputs-notion"
|
"akamai-inputs-notion"
|
||||||
|
"bestcupcakerecipes"
|
||||||
|
"usmc1"
|
||||||
|
"test"
|
||||||
|
"admin"
|
||||||
|
"dev"
|
||||||
|
"staging"
|
||||||
|
"beta"
|
||||||
|
"demo"
|
||||||
|
"old"
|
||||||
|
"new"
|
||||||
|
"backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Функция для создания WAF конфигурации
|
||||||
|
create_waf_config() {
|
||||||
|
docker exec "$NGINX_CONTAINER" sh -c "
|
||||||
|
cat > $WAF_CONF_FILE << 'EOF'
|
||||||
|
# WAF конфигурация для блокировки подозрительных IP
|
||||||
|
geo \$bad_ip {
|
||||||
|
default 0;
|
||||||
|
# Заблокированные IP будут добавляться сюда автоматически
|
||||||
|
EOF
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
# Функция для блокировки IP
|
# Функция для блокировки IP
|
||||||
block_ip() {
|
block_ip() {
|
||||||
local ip=$1
|
local ip=$1
|
||||||
@@ -51,9 +76,16 @@ block_ip() {
|
|||||||
echo "$ip" >> "$BLOCKED_IPS_FILE"
|
echo "$ip" >> "$BLOCKED_IPS_FILE"
|
||||||
echo "🚫 Блокируем IP: $ip (причина: $reason)"
|
echo "🚫 Блокируем IP: $ip (причина: $reason)"
|
||||||
|
|
||||||
# Добавляем IP в nginx конфигурацию
|
# Добавляем IP в nginx WAF конфигурацию
|
||||||
docker exec "$NGINX_CONTAINER" sh -c "
|
docker exec "$NGINX_CONTAINER" sh -c "
|
||||||
echo ' $ip 1; # Автоматически заблокирован: $reason' >> /etc/nginx/conf.d/waf.conf
|
if [ ! -f $WAF_CONF_FILE ]; then
|
||||||
|
create_waf_config
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Добавляем IP в WAF конфигурацию
|
||||||
|
sed -i '/default 0;/a\\ $ip 1; # Автоматически заблокирован: $reason' $WAF_CONF_FILE
|
||||||
|
|
||||||
|
# Перезагружаем nginx
|
||||||
nginx -s reload
|
nginx -s reload
|
||||||
"
|
"
|
||||||
|
|
||||||
@@ -79,46 +111,56 @@ log_suspicious_domain() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Функция для анализа логов
|
# Функция для анализа Docker логов nginx
|
||||||
analyze_logs() {
|
analyze_docker_logs() {
|
||||||
echo "🔍 Анализ логов на предмет атак..."
|
echo "🔍 Анализ Docker логов nginx на предмет атак..."
|
||||||
|
|
||||||
# Ищем подозрительные запросы
|
# Анализируем логи nginx контейнера
|
||||||
docker exec "$NGINX_CONTAINER" tail -f "$LOG_FILE" | while read line; do
|
docker logs --follow "$NGINX_CONTAINER" | while read line; do
|
||||||
|
# Ищем HTTP запросы в логах
|
||||||
|
if echo "$line" | grep -qE "(GET|POST|HEAD|PUT|DELETE|OPTIONS)"; then
|
||||||
# Извлекаем IP адрес
|
# Извлекаем IP адрес
|
||||||
ip=$(echo "$line" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}')
|
ip=$(echo "$line" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}')
|
||||||
|
|
||||||
# Извлекаем домен из Referer
|
# Извлекаем домен из Host заголовка
|
||||||
domain=$(echo "$line" | grep -oE 'https?://[^/]+' | sed 's|https\?://||')
|
domain=$(echo "$line" | grep -oE 'Host: [^[:space:]]+' | sed 's/Host: //')
|
||||||
|
|
||||||
|
# Извлекаем User-Agent
|
||||||
|
user_agent=$(echo "$line" | grep -oE 'User-Agent: [^[:space:]]+' | sed 's/User-Agent: //')
|
||||||
|
|
||||||
|
# Извлекаем URI
|
||||||
|
uri=$(echo "$line" | grep -oE '(GET|POST|HEAD|PUT|DELETE|OPTIONS) [^[:space:]]+' | awk '{print $2}')
|
||||||
|
|
||||||
if [ -n "$ip" ]; then
|
if [ -n "$ip" ]; then
|
||||||
|
echo "🔍 Анализируем запрос: $ip -> $domain -> $uri"
|
||||||
|
|
||||||
# Проверяем на подозрительные запросы
|
# Проверяем на подозрительные запросы
|
||||||
if echo "$line" | grep -q "\.env\|\.config\|\.ini\|\.sql\|\.bak\|\.log"; then
|
if echo "$uri" | grep -q "\.env\|\.config\|\.ini\|\.sql\|\.bak\|\.log"; then
|
||||||
block_ip "$ip" "Попытка доступа к чувствительным файлам"
|
block_ip "$ip" "Попытка доступа к чувствительным файлам: $uri"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем на сканирование резервных копий и архивов
|
# Проверяем на сканирование резервных копий и архивов
|
||||||
if echo "$line" | grep -q "backup\|backups\|bak\|old\|restore\|\.tar\|\.gz\|sftp-config"; then
|
if echo "$uri" | grep -q "backup\|backups\|bak\|old\|restore\|\.tar\|\.gz\|sftp-config"; then
|
||||||
block_ip "$ip" "Сканирование резервных копий и конфигурационных файлов"
|
block_ip "$ip" "Сканирование резервных копий и конфигурационных файлов: $uri"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем на подозрительные поддомены
|
# Проверяем на подозрительные поддомены
|
||||||
if echo "$line" | grep -q "bestcupcakerecipes\|usmc1\|test\|admin\|dev\|staging"; then
|
if echo "$domain" | grep -q "bestcupcakerecipes\|usmc1\|test\|admin\|dev\|staging"; then
|
||||||
block_ip "$ip" "Попытка доступа к несуществующим поддоменам"
|
block_ip "$ip" "Попытка доступа к несуществующим поддоменам: $domain"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем на старые User-Agent
|
# Проверяем на старые User-Agent
|
||||||
if echo "$line" | grep -q "Chrome/[1-7][0-9]\."; then
|
if echo "$user_agent" | grep -q "Chrome/[1-7][0-9]\."; then
|
||||||
block_ip "$ip" "Подозрительный User-Agent (старый Chrome)"
|
block_ip "$ip" "Подозрительный User-Agent (старый Chrome): $user_agent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if echo "$line" | grep -q "Safari/[1-5][0-9][0-9]\."; then
|
if echo "$user_agent" | grep -q "Safari/[1-5][0-9][0-9]\."; then
|
||||||
block_ip "$ip" "Подозрительный User-Agent (старый Safari)"
|
block_ip "$ip" "Подозрительный User-Agent (старый Safari): $user_agent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем на известные сканеры
|
# Проверяем на известные сканеры
|
||||||
if echo "$line" | grep -qi "bot\|crawler\|spider\|scanner\|nmap\|sqlmap"; then
|
if echo "$user_agent" | grep -qi "bot\|crawler\|spider\|scanner\|nmap\|sqlmap"; then
|
||||||
block_ip "$ip" "Известный сканер/бот"
|
block_ip "$ip" "Известный сканер/бот: $user_agent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверяем на подозрительные домены
|
# Проверяем на подозрительные домены
|
||||||
@@ -130,11 +172,12 @@ analyze_logs() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Проверяем на множественные запросы (DDoS)
|
# Проверяем на множественные запросы (DDoS)
|
||||||
request_count=$(docker exec "$NGINX_CONTAINER" grep "$ip" "$LOG_FILE" | wc -l)
|
request_count=$(docker logs "$NGINX_CONTAINER" | grep "$ip" | wc -l)
|
||||||
if [ "$request_count" -gt 100 ]; then
|
if [ "$request_count" -gt 100 ]; then
|
||||||
block_ip "$ip" "Подозрение на DDoS ($request_count запросов)"
|
block_ip "$ip" "Подозрение на DDoS ($request_count запросов)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,12 +194,16 @@ show_stats() {
|
|||||||
tail -5 "$SUSPICIOUS_DOMAINS_FILE" 2>/dev/null || echo "Нет подозрительных доменов"
|
tail -5 "$SUSPICIOUS_DOMAINS_FILE" 2>/dev/null || echo "Нет подозрительных доменов"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Инициализация WAF конфигурации
|
||||||
|
echo "🔧 Инициализация WAF конфигурации..."
|
||||||
|
create_waf_config
|
||||||
|
|
||||||
# Основной цикл
|
# Основной цикл
|
||||||
while true; do
|
while true; do
|
||||||
echo "🔄 Проверка безопасности... $(date)"
|
echo "🔄 Проверка безопасности... $(date)"
|
||||||
|
|
||||||
# Анализируем логи в фоне
|
# Анализируем логи в фоне
|
||||||
analyze_logs &
|
analyze_docker_logs &
|
||||||
|
|
||||||
# Показываем статистику каждые 5 минут
|
# Показываем статистику каждые 5 минут
|
||||||
show_stats
|
show_stats
|
||||||
|
|||||||
Reference in New Issue
Block a user