whistle.quick-trace 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/README.md +1 -0
- package/dist/resStatsServer.js +78 -0
- package/dist/uiServer.js +115 -0
- package/index.js +3 -0
- package/package.json +14 -0
- package/public/index.html +370 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# whistle.quick-trace
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
function resolveLogFilePath(options) {
|
|
39
|
+
const storagePath = options.storage.getProperty('logFilePath');
|
|
40
|
+
if (storagePath && typeof storagePath === 'string' && storagePath.trim()) {
|
|
41
|
+
return resolveAbsolutePath(storagePath.trim());
|
|
42
|
+
}
|
|
43
|
+
throw new Error('Log file path not configured. Please set logFilePath in Whistle storage.');
|
|
44
|
+
}
|
|
45
|
+
function resolveAbsolutePath(filePath) {
|
|
46
|
+
if (path.isAbsolute(filePath)) {
|
|
47
|
+
return filePath;
|
|
48
|
+
}
|
|
49
|
+
return path.resolve(process.cwd(), filePath);
|
|
50
|
+
}
|
|
51
|
+
function ensureDirectoryExists(filePath) {
|
|
52
|
+
const directory = path.dirname(filePath);
|
|
53
|
+
if (!fs.existsSync(directory)) {
|
|
54
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeLogToFile(filePath, logMessage) {
|
|
58
|
+
try {
|
|
59
|
+
fs.appendFileSync(filePath, logMessage + '\n', 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error('Failed to write log to file:', error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.default = (server, options) => {
|
|
66
|
+
console.log('[quick-trace] Initialized');
|
|
67
|
+
server.on('request', (req) => {
|
|
68
|
+
const logFilePath = resolveLogFilePath(options);
|
|
69
|
+
ensureDirectoryExists(logFilePath);
|
|
70
|
+
const { originalReq, originalRes } = req;
|
|
71
|
+
const baseUrl = originalReq.fullUrl.split('?')[0];
|
|
72
|
+
const eagleEyeId = originalReq.headers['x-eagleeye-id'] || '';
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const timestamp = now.toISOString().replace('T', ' ').replace('Z', '');
|
|
75
|
+
const logMessage = `${timestamp} ${eagleEyeId} ${baseUrl}`;
|
|
76
|
+
writeLogToFile(logFilePath, logMessage);
|
|
77
|
+
});
|
|
78
|
+
};
|
package/dist/uiServer.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const PUBLIC_DIR = path.join(__dirname, '../public');
|
|
39
|
+
function serveStaticFile(filePath, res) {
|
|
40
|
+
const extToMime = {
|
|
41
|
+
'.html': 'text/html; charset=utf-8',
|
|
42
|
+
'.css': 'text/css; charset=utf-8',
|
|
43
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
44
|
+
'.json': 'application/json; charset=utf-8',
|
|
45
|
+
'.png': 'image/png',
|
|
46
|
+
'.jpg': 'image/jpeg',
|
|
47
|
+
'.svg': 'image/svg+xml',
|
|
48
|
+
'.ico': 'image/x-icon',
|
|
49
|
+
};
|
|
50
|
+
const ext = path.extname(filePath);
|
|
51
|
+
const contentType = extToMime[ext] || 'application/octet-stream';
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(filePath);
|
|
54
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
55
|
+
res.end(content);
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
59
|
+
res.end('Not Found');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function readRequestBody(req) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
66
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
67
|
+
req.on('error', reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
exports.default = (server, options) => {
|
|
71
|
+
const { storage } = options;
|
|
72
|
+
server.on('request', (req, res) => {
|
|
73
|
+
const urlPath = (req.url || '/').split('?')[0];
|
|
74
|
+
if (urlPath === '/cgi-bin/get-config') {
|
|
75
|
+
const logFilePath = storage.getProperty('logFilePath') || '';
|
|
76
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
77
|
+
res.end(JSON.stringify({ logFilePath }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (urlPath === '/cgi-bin/save-config' && req.method === 'POST') {
|
|
81
|
+
readRequestBody(req).then((body) => {
|
|
82
|
+
try {
|
|
83
|
+
const config = JSON.parse(body);
|
|
84
|
+
if (config.logFilePath && typeof config.logFilePath === 'string') {
|
|
85
|
+
storage.setProperty('logFilePath', config.logFilePath.trim());
|
|
86
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
87
|
+
res.end(JSON.stringify({ success: true }));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
91
|
+
res.end(JSON.stringify({ success: false, message: '日志文件路径不能为空' }));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (_a) {
|
|
95
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
96
|
+
res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let filePath;
|
|
102
|
+
if (urlPath === '/' || urlPath === '/index.html') {
|
|
103
|
+
filePath = path.join(PUBLIC_DIR, 'index.html');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
filePath = path.join(PUBLIC_DIR, urlPath);
|
|
107
|
+
}
|
|
108
|
+
if (!filePath.startsWith(PUBLIC_DIR)) {
|
|
109
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
110
|
+
res.end('Forbidden');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
serveStaticFile(filePath, res);
|
|
114
|
+
});
|
|
115
|
+
};
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Quick Trace 配置</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
12
|
+
background: linear-gradient(135deg, #f5f7fa 0%, #e4e9f2 100%);
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: flex-start;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
padding: 32px 16px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.card {
|
|
21
|
+
width: 100%;
|
|
22
|
+
max-width: 480px;
|
|
23
|
+
background: #fff;
|
|
24
|
+
border-radius: 16px;
|
|
25
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04);
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* 顶部渐变条 */
|
|
30
|
+
.card-accent {
|
|
31
|
+
height: 4px;
|
|
32
|
+
background: linear-gradient(90deg, #6366f1, #8b5cf6, #a78bfa);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.card-body { padding: 28px 24px; }
|
|
36
|
+
|
|
37
|
+
/* 标题区 */
|
|
38
|
+
.title-row {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 12px;
|
|
42
|
+
margin-bottom: 24px;
|
|
43
|
+
}
|
|
44
|
+
.title-icon {
|
|
45
|
+
width: 40px; height: 40px;
|
|
46
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
47
|
+
border-radius: 10px;
|
|
48
|
+
display: flex; align-items: center; justify-content: center;
|
|
49
|
+
font-size: 20px;
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
}
|
|
52
|
+
.title-text h1 {
|
|
53
|
+
font-size: 17px;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
color: #1e1e2e;
|
|
56
|
+
}
|
|
57
|
+
.title-text p {
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
color: #94a3b8;
|
|
60
|
+
margin-top: 2px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* 表单 */
|
|
64
|
+
.field { margin-bottom: 22px; }
|
|
65
|
+
.field-label {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 6px;
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
color: #475569;
|
|
72
|
+
margin-bottom: 8px;
|
|
73
|
+
}
|
|
74
|
+
.field-label .icon { font-size: 14px; }
|
|
75
|
+
|
|
76
|
+
.input-box {
|
|
77
|
+
position: relative;
|
|
78
|
+
}
|
|
79
|
+
.input-box input {
|
|
80
|
+
width: 100%;
|
|
81
|
+
padding: 11px 14px;
|
|
82
|
+
border: 1.5px solid #e2e8f0;
|
|
83
|
+
border-radius: 10px;
|
|
84
|
+
font-size: 13px;
|
|
85
|
+
color: #334155;
|
|
86
|
+
background: #f8fafc;
|
|
87
|
+
transition: all 0.2s;
|
|
88
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', monospace;
|
|
89
|
+
}
|
|
90
|
+
.input-box input:focus {
|
|
91
|
+
outline: none;
|
|
92
|
+
border-color: #6366f1;
|
|
93
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
94
|
+
background: #fff;
|
|
95
|
+
}
|
|
96
|
+
.input-box input::placeholder { color: #cbd5e1; font-family: inherit; }
|
|
97
|
+
|
|
98
|
+
.field-hint {
|
|
99
|
+
margin-top: 6px;
|
|
100
|
+
font-size: 11px;
|
|
101
|
+
color: #94a3b8;
|
|
102
|
+
line-height: 1.5;
|
|
103
|
+
}
|
|
104
|
+
.field-hint.warning {
|
|
105
|
+
color: #dc2626;
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
background: #fef2f2;
|
|
108
|
+
padding: 8px 12px;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
border: 1px solid #fecaca;
|
|
111
|
+
}
|
|
112
|
+
.field-hint.warning::before {
|
|
113
|
+
content: '⚠️ ';
|
|
114
|
+
margin-right: 4px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* 按钮 */
|
|
118
|
+
.btn-row {
|
|
119
|
+
display: flex;
|
|
120
|
+
gap: 10px;
|
|
121
|
+
margin-top: 24px;
|
|
122
|
+
}
|
|
123
|
+
.btn {
|
|
124
|
+
flex: 1;
|
|
125
|
+
padding: 11px 0;
|
|
126
|
+
border: none;
|
|
127
|
+
border-radius: 10px;
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
transition: all 0.15s;
|
|
132
|
+
letter-spacing: 0.3px;
|
|
133
|
+
}
|
|
134
|
+
.btn:active { transform: scale(0.98); }
|
|
135
|
+
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
136
|
+
|
|
137
|
+
.btn-save {
|
|
138
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
139
|
+
color: #fff;
|
|
140
|
+
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
|
141
|
+
}
|
|
142
|
+
.btn-save:hover:not(:disabled) {
|
|
143
|
+
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4);
|
|
144
|
+
transform: translateY(-1px);
|
|
145
|
+
}
|
|
146
|
+
.btn-reset {
|
|
147
|
+
background: #f1f5f9;
|
|
148
|
+
color: #64748b;
|
|
149
|
+
}
|
|
150
|
+
.btn-reset:hover { background: #e2e8f0; }
|
|
151
|
+
|
|
152
|
+
/* 分隔线 */
|
|
153
|
+
.divider {
|
|
154
|
+
height: 1px;
|
|
155
|
+
background: #f1f5f9;
|
|
156
|
+
margin: 24px 0 18px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* 当前配置 */
|
|
160
|
+
.status-section {}
|
|
161
|
+
.status-label {
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
color: #94a3b8;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
letter-spacing: 0.8px;
|
|
167
|
+
margin-bottom: 8px;
|
|
168
|
+
}
|
|
169
|
+
.status-value {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 8px;
|
|
173
|
+
padding: 10px 14px;
|
|
174
|
+
background: #f8fafc;
|
|
175
|
+
border: 1px solid #f1f5f9;
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
color: #475569;
|
|
179
|
+
word-break: break-all;
|
|
180
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', monospace;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
}
|
|
183
|
+
.status-value .dot {
|
|
184
|
+
width: 6px; height: 6px;
|
|
185
|
+
border-radius: 50%;
|
|
186
|
+
background: #22c55e;
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
}
|
|
189
|
+
.status-value.empty { color: #cbd5e1; font-style: italic; }
|
|
190
|
+
.status-value.empty .dot { background: #e2e8f0; }
|
|
191
|
+
|
|
192
|
+
/* Toast */
|
|
193
|
+
.toast-wrap {
|
|
194
|
+
position: fixed;
|
|
195
|
+
top: 16px;
|
|
196
|
+
left: 50%;
|
|
197
|
+
transform: translateX(-50%) translateY(-100px);
|
|
198
|
+
z-index: 999;
|
|
199
|
+
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s;
|
|
200
|
+
opacity: 0;
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
}
|
|
203
|
+
.toast-wrap.show {
|
|
204
|
+
transform: translateX(-50%) translateY(0);
|
|
205
|
+
opacity: 1;
|
|
206
|
+
}
|
|
207
|
+
.toast-inner {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 8px;
|
|
211
|
+
padding: 10px 20px;
|
|
212
|
+
border-radius: 10px;
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
font-weight: 500;
|
|
215
|
+
white-space: nowrap;
|
|
216
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
217
|
+
}
|
|
218
|
+
.toast-wrap.success .toast-inner {
|
|
219
|
+
background: #ecfdf5;
|
|
220
|
+
color: #059669;
|
|
221
|
+
border: 1px solid #a7f3d0;
|
|
222
|
+
}
|
|
223
|
+
.toast-wrap.error .toast-inner {
|
|
224
|
+
background: #fef2f2;
|
|
225
|
+
color: #dc2626;
|
|
226
|
+
border: 1px solid #fecaca;
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
|
|
232
|
+
<div class="card">
|
|
233
|
+
<div class="card-accent"></div>
|
|
234
|
+
<div class="card-body">
|
|
235
|
+
|
|
236
|
+
<div class="title-row">
|
|
237
|
+
<div class="title-icon">📊</div>
|
|
238
|
+
<div class="title-text">
|
|
239
|
+
<h1>Quick Trace</h1>
|
|
240
|
+
<p>请求日志记录配置</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="field">
|
|
245
|
+
<div class="field-label">
|
|
246
|
+
<span class="icon">📁</span>
|
|
247
|
+
<span>日志文件路径</span>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="input-box">
|
|
250
|
+
<input type="text" id="logFilePath" placeholder="/path/to/your/log/file.log" />
|
|
251
|
+
</div>
|
|
252
|
+
<div class="field-hint warning">请输入本地存储目录(需确保该目录存在,插件不会自动创建)</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div class="btn-row">
|
|
256
|
+
<button class="btn btn-save" id="saveBtn">保存配置</button>
|
|
257
|
+
<button class="btn btn-reset" id="resetBtn">重置</button>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="divider"></div>
|
|
261
|
+
|
|
262
|
+
<div class="status-section">
|
|
263
|
+
<div class="status-label">当前生效配置</div>
|
|
264
|
+
<div class="status-value" id="statusValue">
|
|
265
|
+
<span class="dot"></span>
|
|
266
|
+
<span id="currentValue">加载中...</span>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="toast-wrap" id="toastWrap">
|
|
274
|
+
<div class="toast-inner" id="toastInner"></div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<script>
|
|
278
|
+
var logFilePathInput = document.getElementById('logFilePath');
|
|
279
|
+
var saveBtn = document.getElementById('saveBtn');
|
|
280
|
+
var resetBtn = document.getElementById('resetBtn');
|
|
281
|
+
var toastWrap = document.getElementById('toastWrap');
|
|
282
|
+
var toastInner = document.getElementById('toastInner');
|
|
283
|
+
var statusValue = document.getElementById('statusValue');
|
|
284
|
+
var currentValue = document.getElementById('currentValue');
|
|
285
|
+
var toastTimer = null;
|
|
286
|
+
|
|
287
|
+
function showToast(text, type) {
|
|
288
|
+
clearTimeout(toastTimer);
|
|
289
|
+
toastInner.textContent = (type === 'success' ? '✅ ' : '❌ ') + text;
|
|
290
|
+
toastWrap.className = 'toast-wrap ' + type + ' show';
|
|
291
|
+
toastTimer = setTimeout(function () {
|
|
292
|
+
toastWrap.className = 'toast-wrap';
|
|
293
|
+
}, 2800);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function updateStatus(path) {
|
|
297
|
+
if (path) {
|
|
298
|
+
currentValue.textContent = path;
|
|
299
|
+
statusValue.className = 'status-value';
|
|
300
|
+
} else {
|
|
301
|
+
currentValue.textContent = '未配置(使用默认路径)';
|
|
302
|
+
statusValue.className = 'status-value empty';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function loadConfig() {
|
|
307
|
+
fetch('./cgi-bin/get-config')
|
|
308
|
+
.then(function (res) { return res.json(); })
|
|
309
|
+
.then(function (data) {
|
|
310
|
+
if (data.logFilePath) {
|
|
311
|
+
logFilePathInput.value = data.logFilePath;
|
|
312
|
+
}
|
|
313
|
+
updateStatus(data.logFilePath);
|
|
314
|
+
})
|
|
315
|
+
.catch(function () {
|
|
316
|
+
currentValue.textContent = '加载失败';
|
|
317
|
+
statusValue.className = 'status-value empty';
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function saveConfig() {
|
|
322
|
+
var logFilePath = logFilePathInput.value.trim();
|
|
323
|
+
if (!logFilePath) {
|
|
324
|
+
showToast('请输入日志文件路径', 'error');
|
|
325
|
+
logFilePathInput.focus();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
saveBtn.disabled = true;
|
|
330
|
+
saveBtn.textContent = '保存中...';
|
|
331
|
+
|
|
332
|
+
fetch('./cgi-bin/save-config', {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: { 'Content-Type': 'application/json' },
|
|
335
|
+
body: JSON.stringify({ logFilePath: logFilePath })
|
|
336
|
+
})
|
|
337
|
+
.then(function (res) { return res.json(); })
|
|
338
|
+
.then(function (data) {
|
|
339
|
+
if (data.success) {
|
|
340
|
+
showToast('保存成功,已立即生效', 'success');
|
|
341
|
+
updateStatus(logFilePath);
|
|
342
|
+
} else {
|
|
343
|
+
showToast(data.message || '保存失败', 'error');
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
.catch(function () {
|
|
347
|
+
showToast('网络错误,请重试', 'error');
|
|
348
|
+
})
|
|
349
|
+
.finally(function () {
|
|
350
|
+
saveBtn.disabled = false;
|
|
351
|
+
saveBtn.textContent = '保存配置';
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
saveBtn.addEventListener('click', saveConfig);
|
|
356
|
+
resetBtn.addEventListener('click', function () {
|
|
357
|
+
logFilePathInput.value = '';
|
|
358
|
+
logFilePathInput.focus();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
logFilePathInput.addEventListener('keydown', function (event) {
|
|
362
|
+
if (event.key === 'Enter') {
|
|
363
|
+
saveConfig();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
loadConfig();
|
|
368
|
+
</script>
|
|
369
|
+
</body>
|
|
370
|
+
</html>
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "./src",
|
|
4
|
+
"outDir": "./dist/",
|
|
5
|
+
"noImplicitAny": true,
|
|
6
|
+
"removeComments": true,
|
|
7
|
+
"module": "commonjs",
|
|
8
|
+
"target": "es6",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"moduleResolution": "node"
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src"
|
|
16
|
+
]
|
|
17
|
+
}
|