mr-sliy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +145 -0
- package/database/schema.sql +187 -0
- package/package.json +74 -0
- package/scripts/download-tree-sitter.js +171 -0
- package/scripts/postinstall.js +134 -0
- package/src/agent/agent.js +563 -0
- package/src/agent.js +87 -0
- package/src/cli/index.js +1643 -0
- package/src/config/index.js +232 -0
- package/src/engine/dualModeEngine.js +486 -0
- package/src/index.js +165 -0
- package/src/middlewares/errorHandler.js +166 -0
- package/src/middlewares/index.js +23 -0
- package/src/routes/aiRoutes.js +117 -0
- package/src/routes/configRoutes.js +31 -0
- package/src/routes/index.js +75 -0
- package/src/routes/issueRoutes.js +195 -0
- package/src/routes/projectRoutes.js +46 -0
- package/src/routes/reportRoutes.js +40 -0
- package/src/routes/scanRoutes.js +245 -0
- package/src/routes/userRoutes.js +47 -0
- package/src/services/ast/parser.js +503 -0
- package/src/services/detection/detector.js +934 -0
- package/src/services/llm/providers.js +1107 -0
- package/src/services/rag/agent.js +375 -0
- package/src/services/vector/knowledgeBase.js +863 -0
- package/src/skills/Skill.js +38 -0
- package/src/skills/code-analysis/index.js +272 -0
- package/src/skills/code-detection/index.js +166 -0
- package/src/skills/code-detection/rules/console-log.js +45 -0
- package/src/skills/code-detection/rules/deep-nesting.js +76 -0
- package/src/skills/code-detection/rules/duplicate-code.js +57 -0
- package/src/skills/code-detection/rules/high-complexity.js +109 -0
- package/src/skills/code-detection/rules/index.js +59 -0
- package/src/skills/code-detection/rules/long-functions.js +54 -0
- package/src/skills/code-detection/rules/magic-numbers.js +48 -0
- package/src/skills/code-detection/rules/missing-comment.js +64 -0
- package/src/skills/code-detection/rules/null-check.js +71 -0
- package/src/skills/code-detection/rules/unnecessary-else.js +46 -0
- package/src/skills/code-detection/rules/unused-functions.js +57 -0
- package/src/skills/code-detection/rules/unused-imports.js +57 -0
- package/src/skills/code-detection/rules/unused-variables.js +54 -0
- package/src/skills/code-optimization/index.js +319 -0
- package/src/skills/index.js +152 -0
- package/src/utils/crypto.js +212 -0
- package/src/utils/database.js +125 -0
- package/src/utils/helpers.js +226 -0
- package/src/utils/logger.js +202 -0
- package/src/utils/mysql.js +198 -0
- package/src/utils/response.js +124 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库连接模块
|
|
3
|
+
* 使用better-sqlite3管理SQLite数据库
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Database = require('better-sqlite3');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { config } = require('../config');
|
|
10
|
+
|
|
11
|
+
let dbInstance = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取数据库实例(单例模式)
|
|
15
|
+
*/
|
|
16
|
+
function getDatabase() {
|
|
17
|
+
if (!dbInstance) {
|
|
18
|
+
let dbPath = config.database.path;
|
|
19
|
+
|
|
20
|
+
if (!path.isAbsolute(dbPath)) {
|
|
21
|
+
dbPath = path.resolve(path.join(__dirname, '../../', dbPath));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dbDir = path.dirname(dbPath);
|
|
25
|
+
if (!fs.existsSync(dbDir)) {
|
|
26
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
dbInstance = new Database(dbPath);
|
|
30
|
+
|
|
31
|
+
dbInstance.pragma('foreign_keys = ON');
|
|
32
|
+
dbInstance.pragma('journal_mode = WAL');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return dbInstance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 关闭数据库连接
|
|
40
|
+
*/
|
|
41
|
+
function closeDatabase() {
|
|
42
|
+
if (dbInstance) {
|
|
43
|
+
dbInstance.close();
|
|
44
|
+
dbInstance = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 执行查询(返回所有结果)
|
|
50
|
+
*/
|
|
51
|
+
function query(sql, params = []) {
|
|
52
|
+
const db = getDatabase();
|
|
53
|
+
try {
|
|
54
|
+
const stmt = db.prepare(sql);
|
|
55
|
+
return stmt.all(...params);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 执行查询(返回单条结果)
|
|
63
|
+
*/
|
|
64
|
+
function queryOne(sql, params = []) {
|
|
65
|
+
const db = getDatabase();
|
|
66
|
+
try {
|
|
67
|
+
const stmt = db.prepare(sql);
|
|
68
|
+
return stmt.get(...params);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 执行插入/更新/删除操作
|
|
76
|
+
*/
|
|
77
|
+
function execute(sql, params = []) {
|
|
78
|
+
const db = getDatabase();
|
|
79
|
+
try {
|
|
80
|
+
const stmt = db.prepare(sql);
|
|
81
|
+
const result = stmt.run(...params);
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
changes: result.changes,
|
|
85
|
+
lastInsertRowid: result.lastInsertRowid
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 执行事务
|
|
94
|
+
*/
|
|
95
|
+
function transaction(callback) {
|
|
96
|
+
const db = getDatabase();
|
|
97
|
+
const txn = db.transaction(callback);
|
|
98
|
+
return txn();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 批量执行
|
|
103
|
+
*/
|
|
104
|
+
function batchExecute(sqlArray, paramsArray) {
|
|
105
|
+
const db = getDatabase();
|
|
106
|
+
const stmt = db.prepare(sqlArray);
|
|
107
|
+
|
|
108
|
+
const insertMany = db.transaction((items) => {
|
|
109
|
+
for (const params of items) {
|
|
110
|
+
stmt.run(...params);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return insertMany(paramsArray);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
getDatabase,
|
|
119
|
+
closeDatabase,
|
|
120
|
+
query,
|
|
121
|
+
queryOne,
|
|
122
|
+
execute,
|
|
123
|
+
transaction,
|
|
124
|
+
batchExecute
|
|
125
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用工具函数模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 生成UUID
|
|
10
|
+
*/
|
|
11
|
+
function generateUUID() {
|
|
12
|
+
return crypto.randomUUID();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 延迟函数
|
|
17
|
+
*/
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 深拷贝对象
|
|
24
|
+
*/
|
|
25
|
+
function deepClone(obj) {
|
|
26
|
+
return JSON.parse(JSON.stringify(obj));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 格式化日期时间
|
|
31
|
+
*/
|
|
32
|
+
function formatDateTime(date = new Date()) {
|
|
33
|
+
const d = new Date(date);
|
|
34
|
+
const year = d.getFullYear();
|
|
35
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
36
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
37
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
38
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
39
|
+
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
40
|
+
|
|
41
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 格式化文件大小
|
|
46
|
+
*/
|
|
47
|
+
function formatFileSize(bytes) {
|
|
48
|
+
if (bytes === 0) return '0 B';
|
|
49
|
+
|
|
50
|
+
const k = 1024;
|
|
51
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
52
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
53
|
+
|
|
54
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 格式化持续时间
|
|
59
|
+
*/
|
|
60
|
+
function formatDuration(ms) {
|
|
61
|
+
if (ms < 1000) return `${ms}ms`;
|
|
62
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
|
63
|
+
if (ms < 3600000) return `${(ms / 60000).toFixed(2)}min`;
|
|
64
|
+
return `${(ms / 3600000).toFixed(2)}h`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 获取文件扩展名
|
|
69
|
+
*/
|
|
70
|
+
function getFileExtension(filename) {
|
|
71
|
+
return path.extname(filename).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 获取文件语言类型
|
|
76
|
+
*/
|
|
77
|
+
function getFileLanguage(filename) {
|
|
78
|
+
const ext = getFileExtension(filename);
|
|
79
|
+
const languageMap = {
|
|
80
|
+
'.js': 'javascript',
|
|
81
|
+
'.jsx': 'javascript',
|
|
82
|
+
'.ts': 'typescript',
|
|
83
|
+
'.tsx': 'typescript',
|
|
84
|
+
'.py': 'python',
|
|
85
|
+
'.java': 'java',
|
|
86
|
+
'.go': 'go',
|
|
87
|
+
'.rs': 'rust',
|
|
88
|
+
'.c': 'c',
|
|
89
|
+
'.cpp': 'cpp',
|
|
90
|
+
'.h': 'c',
|
|
91
|
+
'.hpp': 'cpp',
|
|
92
|
+
'.cs': 'csharp',
|
|
93
|
+
'.rb': 'ruby',
|
|
94
|
+
'.php': 'php',
|
|
95
|
+
'.swift': 'swift',
|
|
96
|
+
'.kt': 'kotlin',
|
|
97
|
+
'.scala': 'scala'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return languageMap[ext] || 'unknown';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 检查文件是否为支持的扫描类型
|
|
105
|
+
*/
|
|
106
|
+
function isSupportedFile(filename, extensions) {
|
|
107
|
+
const ext = getFileExtension(filename);
|
|
108
|
+
return extensions.includes(ext);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 检查路径是否在排除目录中
|
|
113
|
+
*/
|
|
114
|
+
function isExcludedPath(filePath, excludeDirs, excludeFiles) {
|
|
115
|
+
const parts = filePath.split(/[/\\]/);
|
|
116
|
+
|
|
117
|
+
// 检查排除目录
|
|
118
|
+
for (const dir of excludeDirs) {
|
|
119
|
+
if (parts.includes(dir)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 检查排除文件
|
|
125
|
+
const filename = parts[parts.length - 1];
|
|
126
|
+
for (const file of excludeFiles) {
|
|
127
|
+
if (filename.includes(file)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 计算代码行数
|
|
137
|
+
*/
|
|
138
|
+
function countLines(code) {
|
|
139
|
+
if (!code) return 0;
|
|
140
|
+
return code.split('\n').length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 安全解析JSON
|
|
145
|
+
*/
|
|
146
|
+
function safeJsonParse(str, defaultValue = null) {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(str);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return defaultValue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 安全字符串化JSON
|
|
156
|
+
*/
|
|
157
|
+
function safeJsonStringify(obj, defaultValue = '{}') {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.stringify(obj);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return defaultValue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 批量处理数组(分块)
|
|
167
|
+
*/
|
|
168
|
+
function chunk(array, size) {
|
|
169
|
+
const chunks = [];
|
|
170
|
+
for (let i = 0; i < array.length; i += size) {
|
|
171
|
+
chunks.push(array.slice(i, i + size));
|
|
172
|
+
}
|
|
173
|
+
return chunks;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 重试函数
|
|
178
|
+
*/
|
|
179
|
+
async function retry(fn, maxRetries = 3, delay = 1000) {
|
|
180
|
+
let lastError;
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
183
|
+
try {
|
|
184
|
+
return await fn();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
lastError = error;
|
|
187
|
+
if (i < maxRetries - 1) {
|
|
188
|
+
await sleep(delay);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw lastError;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 简单哈希函数
|
|
198
|
+
*/
|
|
199
|
+
function simpleHash(str) {
|
|
200
|
+
let hash = 0;
|
|
201
|
+
for (let i = 0; i < str.length; i++) {
|
|
202
|
+
const char = str.charCodeAt(i);
|
|
203
|
+
hash = ((hash << 5) - hash) + char;
|
|
204
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
205
|
+
}
|
|
206
|
+
return hash;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
generateUUID,
|
|
211
|
+
sleep,
|
|
212
|
+
deepClone,
|
|
213
|
+
formatDateTime,
|
|
214
|
+
formatFileSize,
|
|
215
|
+
formatDuration,
|
|
216
|
+
getFileExtension,
|
|
217
|
+
getFileLanguage,
|
|
218
|
+
isSupportedFile,
|
|
219
|
+
isExcludedPath,
|
|
220
|
+
countLines,
|
|
221
|
+
safeJsonParse,
|
|
222
|
+
safeJsonStringify,
|
|
223
|
+
chunk,
|
|
224
|
+
retry,
|
|
225
|
+
simpleHash
|
|
226
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志工具模块
|
|
3
|
+
* 使用winston实现多级别日志记录
|
|
4
|
+
* 包含敏感数据自动脱敏功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const winston = require('winston');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const { config } = require('../config');
|
|
11
|
+
|
|
12
|
+
// 确保日志目录存在
|
|
13
|
+
const logDir = path.dirname(config.logging.file);
|
|
14
|
+
if (!fs.existsSync(logDir)) {
|
|
15
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 敏感字段列表(日志输出时自动脱敏)
|
|
19
|
+
const SENSITIVE_FIELDS = [
|
|
20
|
+
'password', 'passwd', 'pwd',
|
|
21
|
+
'apiKey', 'api_key', 'apikey',
|
|
22
|
+
'secret', 'secretKey', 'secret_key',
|
|
23
|
+
'token', 'accessToken', 'access_token',
|
|
24
|
+
'refreshToken', 'refresh_token',
|
|
25
|
+
'authorization', 'auth',
|
|
26
|
+
'privateKey', 'private_key',
|
|
27
|
+
'credential', 'credentials'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 递归脱敏对象中的敏感字段
|
|
32
|
+
*/
|
|
33
|
+
function maskSensitiveData(obj, depth = 0) {
|
|
34
|
+
if (depth > 5 || obj === null || obj === undefined) {
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof obj === 'string') {
|
|
39
|
+
// 检查字符串是否可能是API key或token
|
|
40
|
+
if (obj.length > 20 && /^(sk-|pk-|eyJ|xox[baprs]-)/i.test(obj)) {
|
|
41
|
+
return maskString(obj);
|
|
42
|
+
}
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof obj !== 'object') {
|
|
47
|
+
return obj;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(obj)) {
|
|
51
|
+
return obj.map(item => maskSensitiveData(item, depth + 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
56
|
+
const lowerKey = key.toLowerCase();
|
|
57
|
+
if (SENSITIVE_FIELDS.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
58
|
+
result[key] = typeof value === 'string' ? maskString(value) : '[REDACTED]';
|
|
59
|
+
} else {
|
|
60
|
+
result[key] = maskSensitiveData(value, depth + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 脱敏字符串(显示前4后4,中间用*代替)
|
|
68
|
+
*/
|
|
69
|
+
function maskString(str) {
|
|
70
|
+
if (!str || str.length <= 8) {
|
|
71
|
+
return '****';
|
|
72
|
+
}
|
|
73
|
+
return str.substring(0, 4) + '*'.repeat(str.length - 8) + str.substring(str.length - 4);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 定义日志格式
|
|
77
|
+
const logFormat = winston.format.combine(
|
|
78
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
79
|
+
winston.format.errors({ stack: true }),
|
|
80
|
+
winston.format((info) => {
|
|
81
|
+
// 脱敏所有日志消息中的敏感数据
|
|
82
|
+
if (info.message && typeof info.message === 'object') {
|
|
83
|
+
info.message = maskSensitiveData(info.message);
|
|
84
|
+
}
|
|
85
|
+
// 脱敏meta数据
|
|
86
|
+
const metaKeys = Object.keys(info).filter(k =>
|
|
87
|
+
!['level', 'message', 'timestamp', 'stack', 'label', 'durationMs'].includes(k)
|
|
88
|
+
);
|
|
89
|
+
for (const key of metaKeys) {
|
|
90
|
+
info[key] = maskSensitiveData(info[key]);
|
|
91
|
+
}
|
|
92
|
+
return info;
|
|
93
|
+
})(),
|
|
94
|
+
winston.format.printf(({ level, message, timestamp, stack }) => {
|
|
95
|
+
if (stack) {
|
|
96
|
+
return `${timestamp} [${level.toUpperCase()}]: ${typeof message === 'object' ? JSON.stringify(message) : message}\n${stack}`;
|
|
97
|
+
}
|
|
98
|
+
return `${timestamp} [${level.toUpperCase()}]: ${typeof message === 'object' ? JSON.stringify(message) : message}`;
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// 控制台日志格式(带颜色)
|
|
103
|
+
const consoleFormat = winston.format.combine(
|
|
104
|
+
winston.format.colorize(),
|
|
105
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
106
|
+
winston.format.printf(({ level, message, timestamp }) => {
|
|
107
|
+
return `${timestamp} [${level}]: ${message}`;
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// 创建logger实例
|
|
112
|
+
const logger = winston.createLogger({
|
|
113
|
+
level: config.logging.level,
|
|
114
|
+
format: logFormat,
|
|
115
|
+
transports: [
|
|
116
|
+
// 文件日志
|
|
117
|
+
new winston.transports.File({
|
|
118
|
+
filename: config.logging.file,
|
|
119
|
+
maxsize: config.logging.maxSize,
|
|
120
|
+
maxFiles: config.logging.maxFiles,
|
|
121
|
+
tailable: true
|
|
122
|
+
}),
|
|
123
|
+
// 错误日志单独文件
|
|
124
|
+
new winston.transports.File({
|
|
125
|
+
filename: path.join(logDir, 'error.log'),
|
|
126
|
+
level: 'error',
|
|
127
|
+
maxsize: config.logging.maxSize,
|
|
128
|
+
maxFiles: config.logging.maxFiles
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 开发环境添加控制台输出(仅显示警告及以上级别)
|
|
134
|
+
if (config.server.nodeEnv !== 'production') {
|
|
135
|
+
logger.add(new winston.transports.Console({
|
|
136
|
+
format: consoleFormat,
|
|
137
|
+
level: 'warn'
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 请求日志中间件
|
|
143
|
+
*/
|
|
144
|
+
function requestLogger(req, res, next) {
|
|
145
|
+
const startTime = Date.now();
|
|
146
|
+
|
|
147
|
+
// 记录请求
|
|
148
|
+
logger.info(`请求开始: ${req.method} ${req.url}`, {
|
|
149
|
+
method: req.method,
|
|
150
|
+
url: req.url,
|
|
151
|
+
ip: req.ip,
|
|
152
|
+
userAgent: req.get('user-agent')
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 记录响应
|
|
156
|
+
res.on('finish', () => {
|
|
157
|
+
const duration = Date.now() - startTime;
|
|
158
|
+
logger.info(`请求完成: ${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
next();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 操作日志记录
|
|
166
|
+
*/
|
|
167
|
+
async function logOperation(data) {
|
|
168
|
+
const db = require('./database').getDatabase();
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const stmt = db.prepare(`
|
|
172
|
+
INSERT INTO sys_oper_log
|
|
173
|
+
(user_id, username, operation_type, operation_desc, request_method,
|
|
174
|
+
request_url, request_params, response_status, ip_address, user_agent, duration_ms)
|
|
175
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
176
|
+
`);
|
|
177
|
+
|
|
178
|
+
stmt.run(
|
|
179
|
+
data.userId || null,
|
|
180
|
+
data.username || 'system',
|
|
181
|
+
data.operationType,
|
|
182
|
+
data.operationDesc,
|
|
183
|
+
data.requestMethod || null,
|
|
184
|
+
data.requestUrl || null,
|
|
185
|
+
data.requestParams ? JSON.stringify(data.requestParams) : null,
|
|
186
|
+
data.responseStatus || null,
|
|
187
|
+
data.ipAddress || null,
|
|
188
|
+
data.userAgent || null,
|
|
189
|
+
data.durationMs || null
|
|
190
|
+
);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error('记录操作日志失败:', error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
logger,
|
|
198
|
+
requestLogger,
|
|
199
|
+
logOperation,
|
|
200
|
+
maskSensitiveData,
|
|
201
|
+
maskString
|
|
202
|
+
};
|