@oxiaom/adoremix 1.0.37 → 1.0.39

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.
@@ -0,0 +1,1396 @@
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
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
7
+ <meta http-equiv="Pragma" content="no-cache">
8
+ <meta http-equiv="Expires" content="0">
9
+ <title>设备配置管理 v1.1</title>
10
+ <!-- SVG 图标定义 -->
11
+ <svg style="display: none;">
12
+ <symbol id="icon-globe" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></symbol>
13
+ <symbol id="icon-clipboard" viewBox="0 0 24 24"><path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></symbol>
14
+ <symbol id="icon-robot" viewBox="0 0 24 24"><path d="M20 9V7c0-1.1-.9-2-2-2h-3c0-1.66-1.34-3-3-3S9 3.34 9 5H6c-1.1 0-2 .9-2 2v2c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h1v-8h2v8h10v-8h2v8h1c1.1 0 2-.9 2-2v-6c0-1.1-.9-2-2-2zM9 13c-.83 0-1.5-.67-1.5-1.5S8.17 10 9 10s1.5.67 1.5 1.5S9.83 13 9 13zm6 0c-.83 0-1.5-.67-1.5-1.5S14.17 10 15 10s1.5.67 1.5 1.5S15.83 13 15 13z"/></symbol>
15
+ <symbol id="icon-brain" viewBox="0 0 24 24"><path d="M13 3c.65 0 1.26.09 1.83.26.38.11.67.43.71.82.1.9.84 1.59 1.75 1.71.45.06.89.24 1.25.55.63.54.96 1.34.96 2.16 0 .82-.33 1.62-.96 2.16-.36.31-.8.49-1.25.55-.91.12-1.65.81-1.75 1.71-.04.39-.33.71-.71.82-.57.17-1.18.26-1.83.26H11c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h2m0-2H11c-2.21 0-4 1.79-4 4v6c0 2.21 1.79 4 4 4h2c.78 0 1.53-.11 2.24-.31.96-.28 1.72-1.04 1.97-2.01.13-.51.52-.91 1.03-1.04.86-.22 1.62-.71 2.16-1.36C21.1 9.5 21.5 8.54 21.5 7.5c0-1.04-.4-2-.95-2.78-.54-.65-1.3-1.14-2.16-1.36-.51-.13-.9-.53-1.03-1.04-.25-.97-1.01-1.73-1.97-2.01C14.53 1.11 13.78 1 13 1zM15.5 10c.83 0 1.5-.67 1.5-1.5S16.33 7 15.5 7 14 7.67 14 8.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 7 8.5 7 7 7.67 7 8.5 7.67 10 8.5 10zm3.5 4c-2.33 0-4.31 1.46-5.11 3.5h10.22c-.8-2.04-2.78-3.5-5.11-3.5zm-7.11 5.5c.8 2.04 2.78 3.5 5.11 3.5h4c2.33 0 4.31-1.46 5.11-3.5H4.89z"/></symbol>
16
+ <symbol id="icon-save" viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></symbol>
17
+ <symbol id="icon-bolt" viewBox="0 0 24 24"><path d="M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z"/></symbol>
18
+ <symbol id="icon-refresh" viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></symbol>
19
+ <symbol id="icon-check" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></symbol>
20
+ <symbol id="icon-x" viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></symbol>
21
+ <symbol id="icon-package" viewBox="0 0 24 24"><path d="M20 2H4c-1 0-2 .9-2 2v3.01c0 .72.43 1.34 1 1.69V20c0 1.1 1.1 2 2 2h14c.9 0 2-.9 2-2V8.7c.57-.35 1-.97 1-1.69V4c0-1.1-1-2-2-2zm-5 12H9v-2h6v2zm5-7H4V4h16v3z"/></symbol>
22
+ <symbol id="icon-location" viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></symbol>
23
+ <symbol id="icon-hash" viewBox="0 0 24 24"><path d="M5 9h14v2H5V9zm0 4h14v2H5v-2zm7-8v14h2V5h-2z"/></symbol>
24
+ <symbol id="icon-door" viewBox="0 0 24 24"><path d="M19 19V5c0-1.1-.9-2-2-2H7c-1.1 0-2 .9-2 2v14H3v2h18v-2h-2zm-4-7c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1z"/></symbol>
25
+ <symbol id="icon-settings" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></symbol>
26
+ <symbol id="icon-warning" viewBox="0 0 24 24"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></symbol>
27
+ </svg>
28
+ <style>
29
+ /* SVG 图标样式 */
30
+ .icon {
31
+ display: inline-block;
32
+ width: 1em;
33
+ height: 1em;
34
+ vertical-align: -0.125em;
35
+ fill: currentColor;
36
+ }
37
+ .icon-globe { fill: #667eea; }
38
+ .icon-clipboard { fill: #667eea; }
39
+ .icon-robot { fill: #667eea; }
40
+ .icon-brain { fill: #667eea; }
41
+ .icon-save { fill: white; }
42
+ .icon-bolt { fill: white; }
43
+ .icon-refresh { fill: white; }
44
+ .icon-check { fill: #28a745; }
45
+ .icon-x { fill: #888; }
46
+ * {
47
+ margin: 0;
48
+ padding: 0;
49
+ box-sizing: border-box;
50
+ }
51
+ body {
52
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
53
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
54
+ min-height: 100vh;
55
+ padding: 20px;
56
+ }
57
+ .container {
58
+ max-width: 1000px;
59
+ margin: 0 auto;
60
+ }
61
+ h1 {
62
+ color: white;
63
+ text-align: center;
64
+ margin-bottom: 30px;
65
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
66
+ }
67
+ .tabs {
68
+ display: flex;
69
+ gap: 10px;
70
+ margin-bottom: 20px;
71
+ }
72
+ .tab {
73
+ padding: 12px 24px;
74
+ background: rgba(255,255,255,0.2);
75
+ color: white;
76
+ border: none;
77
+ border-radius: 8px 8px 0 0;
78
+ cursor: pointer;
79
+ font-size: 16px;
80
+ transition: all 0.3s;
81
+ }
82
+ .tab:hover {
83
+ background: rgba(255,255,255,0.3);
84
+ }
85
+ .tab.active {
86
+ background: white;
87
+ color: #667eea;
88
+ font-weight: 600;
89
+ }
90
+ .panel {
91
+ background: white;
92
+ border-radius: 0 8px 8px 8px;
93
+ padding: 30px;
94
+ display: none;
95
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
96
+ }
97
+ .panel.active {
98
+ display: block;
99
+ }
100
+ h2 {
101
+ color: #333;
102
+ margin-bottom: 20px;
103
+ padding-bottom: 10px;
104
+ border-bottom: 2px solid #667eea;
105
+ }
106
+ .form-group {
107
+ margin-bottom: 20px;
108
+ }
109
+ label {
110
+ display: block;
111
+ margin-bottom: 8px;
112
+ color: #555;
113
+ font-weight: 500;
114
+ }
115
+ input[type="text"],
116
+ input[type="password"],
117
+ input[type="number"],
118
+ select {
119
+ width: 100%;
120
+ padding: 12px;
121
+ border: 2px solid #e0e0e0;
122
+ border-radius: 8px;
123
+ font-size: 16px;
124
+ transition: border-color 0.3s;
125
+ }
126
+ input:focus, select:focus {
127
+ outline: none;
128
+ border-color: #667eea;
129
+ }
130
+ button {
131
+ padding: 12px 30px;
132
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
133
+ color: white;
134
+ border: none;
135
+ border-radius: 8px;
136
+ font-size: 16px;
137
+ cursor: pointer;
138
+ transition: transform 0.2s, box-shadow 0.2s;
139
+ }
140
+ button:hover {
141
+ transform: translateY(-2px);
142
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
143
+ }
144
+ button:active {
145
+ transform: translateY(0);
146
+ }
147
+ .btn-secondary {
148
+ background: #6c757d;
149
+ }
150
+ .btn-danger {
151
+ background: #dc3545;
152
+ }
153
+ .btn-danger:hover {
154
+ box-shadow: 0 5px 15px rgba(220, 53, 69, 0.4);
155
+ }
156
+ .message {
157
+ padding: 15px;
158
+ border-radius: 8px;
159
+ margin-bottom: 20px;
160
+ display: none;
161
+ }
162
+ .message.success {
163
+ background: #d4edda;
164
+ color: #155724;
165
+ border: 1px solid #c3e6cb;
166
+ }
167
+ .message.error {
168
+ background: #f8d7da;
169
+ color: #721c24;
170
+ border: 1px solid #f5c6cb;
171
+ }
172
+ .message.show {
173
+ display: block;
174
+ }
175
+ .config-section {
176
+ background: #f8f9fa;
177
+ padding: 20px;
178
+ border-radius: 8px;
179
+ margin-bottom: 20px;
180
+ }
181
+ .config-section h3 {
182
+ color: #667eea;
183
+ margin-bottom: 15px;
184
+ }
185
+ .config-grid {
186
+ display: grid;
187
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
188
+ gap: 15px;
189
+ }
190
+ .config-item {
191
+ display: flex;
192
+ flex-direction: column;
193
+ }
194
+ .config-item label {
195
+ font-size: 14px;
196
+ color: #666;
197
+ margin-bottom: 5px;
198
+ }
199
+ .config-item input[type="text"] {
200
+ padding: 8px 12px;
201
+ font-size: 14px;
202
+ }
203
+ .config-item input[type="number"] {
204
+ padding: 8px 12px;
205
+ font-size: 14px;
206
+ }
207
+ .config-item .checkbox-wrapper {
208
+ display: flex;
209
+ align-items: center;
210
+ padding: 8px 0;
211
+ }
212
+ .config-item input[type="checkbox"] {
213
+ width: 20px;
214
+ height: 20px;
215
+ cursor: pointer;
216
+ }
217
+ .config-item .checkbox-label {
218
+ margin-left: 8px;
219
+ font-size: 14px;
220
+ color: #666;
221
+ }
222
+ input.invalid {
223
+ border-color: #dc3545 !important;
224
+ background-color: #fff5f5;
225
+ }
226
+ .validation-error {
227
+ color: #dc3545;
228
+ font-size: 12px;
229
+ margin-top: 4px;
230
+ }
231
+ .current-info {
232
+ background: #e7f3ff;
233
+ padding: 15px;
234
+ border-radius: 8px;
235
+ margin-bottom: 20px;
236
+ border-left: 4px solid #667eea;
237
+ }
238
+ .current-info p {
239
+ margin: 5px 0;
240
+ color: #333;
241
+ }
242
+ .loading {
243
+ text-align: center;
244
+ padding: 40px;
245
+ color: #999;
246
+ }
247
+ .button-group {
248
+ display: flex;
249
+ gap: 10px;
250
+ margin-top: 20px;
251
+ }
252
+ @media (max-width: 768px) {
253
+ .tabs {
254
+ flex-wrap: wrap;
255
+ }
256
+ .tab {
257
+ flex: 1;
258
+ min-width: 100px;
259
+ text-align: center;
260
+ }
261
+ .config-grid {
262
+ grid-template-columns: 1fr;
263
+ }
264
+ }
265
+ </style>
266
+ </head>
267
+ <body>
268
+ <div class="container">
269
+ <h1><svg class="icon" style="width:32px;height:32px;vertical-align:middle;"><use xlink:href="#icon-settings"/></svg> 设备配置管理</h1>
270
+
271
+ <div class="tabs">
272
+ <button class="tab active" onclick="showPanel('network')"><svg class="icon"><use xlink:href="#icon-globe"/></svg> 网络配置</button>
273
+ <button class="tab" onclick="showPanel('config')"><svg class="icon"><use xlink:href="#icon-clipboard"/></svg> 应用配置</button>
274
+ <button class="tab" onclick="showPanel('logs')">📜 实时日志</button>
275
+
276
+ </div>
277
+
278
+ <!-- 网络配置面板 -->
279
+ <div id="network-panel" class="panel active">
280
+ <h2>网络配置</h2>
281
+ <div id="network-message" class="message"></div>
282
+
283
+ <!-- 当前网络状态卡片 -->
284
+ <div id="network-status-card" style="border-radius: 12px; padding: 20px; margin-bottom: 24px; display: flex; align-items: center; gap: 16px;">
285
+ <div id="status-icon" style="font-size: 48px;">📡</div>
286
+ <div style="flex: 1;">
287
+ <div id="status-title" style="font-size: 20px; font-weight: 700; margin-bottom: 6px;">正在检测...</div>
288
+ <div id="status-detail" style="color: #666; font-size: 14px;">--</div>
289
+ </div>
290
+ <div id="mode-badge" style="padding: 8px 18px; border-radius: 20px; font-weight: 700; font-size: 16px; letter-spacing: 1px;">
291
+ --
292
+ </div>
293
+ </div>
294
+
295
+ <form id="network-form">
296
+ <div class="form-group">
297
+ <label>网络接口</label>
298
+ <select id="interface" name="interface" disabled>
299
+ <option value="eth0" selected>eth0 (有线网卡)</option>
300
+ </select>
301
+ <small style="color: #888;">仅支持 eth0 有线网卡</small>
302
+ </div>
303
+ <div class="form-group">
304
+ <label>IP 地址</label>
305
+ <input type="text" id="ip" name="ip" placeholder="192.168.1.100">
306
+ </div>
307
+ <div class="form-group">
308
+ <label>子网掩码</label>
309
+ <select id="netmask" name="netmask">
310
+ <option value="8">255.0.0.0 (/8)</option>
311
+ <option value="16">255.255.0.0 (/16)</option>
312
+ <option value="24" selected>255.255.255.0 (/24)</option>
313
+ <option value="25">255.255.255.128 (/25)</option>
314
+ <option value="26">255.255.255.192 (/26)</option>
315
+ <option value="27">255.255.255.224 (/27)</option>
316
+ <option value="28">255.255.255.240 (/28)</option>
317
+ <option value="29">255.255.255.248 (/29)</option>
318
+ <option value="30">255.255.255.252 (/30)</option>
319
+ </select>
320
+ </div>
321
+ <div class="form-group">
322
+ <label>网关</label>
323
+ <input type="text" id="gateway" name="gateway" placeholder="192.168.1.1">
324
+ </div>
325
+ <div class="form-group">
326
+ <label>DNS 服务器</label>
327
+ <input type="text" id="dns" name="dns" value="8.8.8.8" placeholder="8.8.8.8">
328
+ </div>
329
+ <div class="form-group">
330
+ <div id="dhcp-row" style="display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-radius: 10px; border: 2px solid #e0e0e0; cursor: pointer; transition: all 0.3s;" onclick="toggleDhcpClick()">
331
+ <input type="checkbox" id="use-dhcp" name="use-dhcp" onchange="toggleDhcp(this.checked)" style="width: 22px; height: 22px; cursor: pointer;">
332
+ <div style="flex: 1;">
333
+ <div style="font-weight: 600; font-size: 15px;">使用 DHCP(自动获取 IP 地址)</div>
334
+ <small style="color: #888;">勾选后由路由器自动分配 IP,无需手动填写</small>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ <div class="button-group">
339
+ <button type="submit"><svg class="icon"><use xlink:href="#icon-save"/></svg> 保存并应用</button>
340
+ <button type="button" class="btn-secondary" onclick="loadNetwork()"><svg class="icon"><use xlink:href="#icon-refresh"/></svg> 刷新状态</button>
341
+ </div>
342
+ </form>
343
+ <p style="margin-top: 15px; color: #999; font-size: 14px;">
344
+ <svg class="icon icon-x" style="width:14px;height:14px;color:#ff9800;"><use xlink:href="#icon-warning"/></svg> 保存后将自动应用网络配置并重启相关服务
345
+ </p>
346
+ </div>
347
+
348
+ <!-- 应用配置面板 -->
349
+ <div id="config-panel" class="panel">
350
+ <h2>应用配置 (config.ini)</h2>
351
+ <div id="config-message" class="message"></div>
352
+
353
+ <div id="config-container">
354
+ <div class="loading">正在加载配置...</div>
355
+ </div>
356
+
357
+ <div class="button-group">
358
+ <button onclick="saveConfig()"><svg class="icon"><use xlink:href="#icon-save"/></svg> 保存配置</button>
359
+ <button class="btn-secondary" onclick="loadConfig()"><svg class="icon"><use xlink:href="#icon-refresh"/></svg> 重新加载</button>
360
+ <button class="btn-danger" onclick="restartAdoreMix()"><svg class="icon"><use xlink:href="#icon-refresh"/></svg> 重启小播鼠</button>
361
+ </div>
362
+ </div>
363
+
364
+ <!-- QQBot 配置面板 -->
365
+ <div id="qqbot-panel" class="panel">
366
+ <h2>QQBot 配置</h2>
367
+ <div id="qqbot-message" class="message"></div>
368
+
369
+ <div id="qqbot-status-card" style="border-radius: 12px; padding: 20px; margin-bottom: 24px; display: flex; align-items: center; gap: 16px;">
370
+ <div id="qqbot-status-icon" style="font-size: 48px;">🤖</div>
371
+ <div style="flex: 1;">
372
+ <div id="qqbot-status-text" style="font-size: 18px; font-weight: 700;">加载中...</div>
373
+ </div>
374
+ <button id="qqbot-toggle-btn" class="btn-secondary" onclick="toggleQQBot()" style="padding: 10px 20px;">加载中</button>
375
+ </div>
376
+
377
+ <form id="qqbot-form" style="margin-top: 20px;">
378
+ <div class="form-group">
379
+ <label>QQ 号码 (UIN)</label>
380
+ <input type="text" id="qq-uin" name="uin" placeholder="如:123456789">
381
+ </div>
382
+ <div class="form-group">
383
+ <label>QQ 密码</label>
384
+ <input type="password" id="qq-password" name="password" placeholder="请输入密码">
385
+ </div>
386
+ <div class="button-group">
387
+ <button type="submit"><svg class="icon"><use xlink:href="#icon-save"/></svg> 保存配置</button>
388
+ </div>
389
+ </form>
390
+ </div>
391
+
392
+ <!-- 模型配置面板 -->
393
+ <div id="models-panel" class="panel">
394
+ <h2>模型配置</h2>
395
+ <div id="models-message" class="message"></div>
396
+
397
+ <div id="models-container">
398
+ <div class="loading">正在加载配置...</div>
399
+ </div>
400
+
401
+ <div class="button-group" style="margin-top: 20px;">
402
+ <button onclick="saveModels()"><svg class="icon"><use xlink:href="#icon-save"/></svg> 保存配置</button>
403
+ <button class="btn-secondary" onclick="loadModels()"><svg class="icon"><use xlink:href="#icon-refresh"/></svg> 重新加载</button>
404
+ </div>
405
+ </div>
406
+
407
+ <!-- 实时日志面板 -->
408
+ <div id="logs-panel" class="panel">
409
+ <h2>实时日志</h2>
410
+ <div id="logs-message" class="message"></div>
411
+
412
+ <div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 14px;">
413
+ <div style="flex: 0 0 220px;">
414
+ <label>日志源</label>
415
+ <select id="log-service" onchange="restartLogPolling()">
416
+ <optgroup label="📡 systemd journal">
417
+ <option value="adoremix" selected>adoremix (小播鼠)</option>
418
+ <option value="config-manager">config-manager</option>
419
+ <option value="openclaw-gateway">openclaw-gateway</option>
420
+ <option value="smbd">smbd (Samba)</option>
421
+ <option value="nmbd">nmbd (Samba)</option>
422
+ <option value="ssh">ssh</option>
423
+ </optgroup>
424
+ <optgroup label="📁 应用日志文件">
425
+ <option value="adoremix-app">adoremix-app (var/app.log)</option>
426
+ </optgroup>
427
+ </select>
428
+ </div>
429
+ <div style="flex: 0 0 130px;">
430
+ <label>时间范围</label>
431
+ <select id="log-since" onchange="fetchLogs()">
432
+ <option value="5min">最近 5 分钟</option>
433
+ <option value="10min">最近 10 分钟</option>
434
+ <option value="30min">最近 30 分钟</option>
435
+ <option value="1h">最近 1 小时</option>
436
+ <option value="2h" selected>最近 2 小时</option>
437
+ <option value="6h">最近 6 小时</option>
438
+ <option value="today">今天</option>
439
+ <option value="yesterday">昨天</option>
440
+ <option value="all">全部历史</option>
441
+ </select>
442
+ </div>
443
+ <div style="flex: 0 0 100px;">
444
+ <label>行数</label>
445
+ <select id="log-lines" onchange="fetchLogs()">
446
+ <option value="50">50</option>
447
+ <option value="100">100</option>
448
+ <option value="200" selected>200</option>
449
+ <option value="500">500</option>
450
+ <option value="1000">1000</option>
451
+ </select>
452
+ </div>
453
+ <div style="flex: 0 0 110px;">
454
+ <label>刷新间隔</label>
455
+ <select id="log-interval" onchange="restartLogPolling()">
456
+ <option value="1000">1 秒</option>
457
+ <option value="2000" selected>2 秒</option>
458
+ <option value="5000">5 秒</option>
459
+ <option value="10000">10 秒</option>
460
+ <option value="0">手动</option>
461
+ </select>
462
+ </div>
463
+ <div style="flex: 1; min-width: 200px;">
464
+ <label>过滤(高亮匹配关键字)</label>
465
+ <input type="text" id="log-filter" oninput="renderLogs()" placeholder="如:error、failed、Started…">
466
+ </div>
467
+ </div>
468
+
469
+ <div style="display: flex; gap: 18px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; padding: 10px 14px; background: #f5f5f5; border-radius: 8px;">
470
+ <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0; font-size: 14px;">
471
+ <input type="checkbox" id="log-auto-scroll" checked style="width: 18px; height: 18px; cursor: pointer;">
472
+ 自动滚动到底部
473
+ </label>
474
+ <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0; font-size: 14px;">
475
+ <input type="checkbox" id="log-auto-refresh" checked onchange="restartLogPolling()" style="width: 18px; height: 18px; cursor: pointer;">
476
+ 自动刷新
477
+ </label>
478
+ <span id="log-status" style="color: #888; font-size: 13px; margin-left: auto;">就绪</span>
479
+ </div>
480
+
481
+ <div id="log-output" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; line-height: 1.55; height: 480px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; box-shadow: inset 0 2px 6px rgba(0,0,0,0.2);">
482
+ <div style="color: #888;">加载中...</div>
483
+ </div>
484
+
485
+ <div class="button-group" style="margin-top: 16px;">
486
+ <button onclick="fetchLogs()"><svg class="icon"><use xlink:href="#icon-refresh"/></svg> 立即刷新</button>
487
+ <button class="btn-secondary" onclick="copyLogs()">📋 复制到剪贴板</button>
488
+ <button class="btn-secondary" onclick="downloadLogs()">💾 下载日志</button>
489
+ <button class="btn-danger" onclick="clearLogDisplay()">🗑️ 清空显示</button>
490
+ </div>
491
+ </div>
492
+ </div>
493
+
494
+ <script>
495
+ // 切换面板
496
+ function showPanel(name) {
497
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
498
+ document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
499
+
500
+ event.target.classList.add('active');
501
+ document.getElementById(name + '-panel').classList.add('active');
502
+
503
+ // 离开日志面板时停掉轮询,避免后台耗资源
504
+ if (name !== 'logs' && logPollingTimer) {
505
+ clearInterval(logPollingTimer);
506
+ logPollingTimer = null;
507
+ }
508
+
509
+ // 加载数据
510
+ if (name === 'network') loadNetwork();
511
+ if (name === 'config') loadConfig();
512
+ if (name === 'qqbot') loadQQBot();
513
+ if (name === 'models') loadModels();
514
+ if (name === 'logs') restartLogPolling();
515
+ }
516
+
517
+ // 显示消息
518
+ function showMessage(panelId, type, text) {
519
+ const msg = document.getElementById(panelId + '-message');
520
+ msg.className = 'message ' + type + ' show';
521
+ msg.textContent = text;
522
+ setTimeout(() => msg.classList.remove('show'), 5000);
523
+ }
524
+
525
+ // 加载网络配置
526
+ // 点击 DHCP 行切换
527
+ function toggleDhcpClick() {
528
+ const cb = document.getElementById('use-dhcp');
529
+ cb.checked = !cb.checked;
530
+ toggleDhcp(cb.checked);
531
+ }
532
+
533
+ // 切换 DHCP 模式
534
+ function toggleDhcp(enabled) {
535
+ const ipInput = document.getElementById('ip');
536
+ const netmaskSelect = document.getElementById('netmask');
537
+ const gatewayInput = document.getElementById('gateway');
538
+ const dnsInput = document.getElementById('dns');
539
+ const dhcpRow = document.getElementById('dhcp-row');
540
+
541
+ if (enabled) {
542
+ ipInput.disabled = true;
543
+ netmaskSelect.disabled = true;
544
+ gatewayInput.disabled = true;
545
+ dnsInput.disabled = true;
546
+ ipInput.style.backgroundColor = '#f0f0f0';
547
+ netmaskSelect.style.backgroundColor = '#f0f0f0';
548
+ gatewayInput.style.backgroundColor = '#f0f0f0';
549
+ dnsInput.style.backgroundColor = '#f0f0f0';
550
+ dhcpRow.style.borderColor = '#28a745';
551
+ dhcpRow.style.background = '#f0fdf4';
552
+ } else {
553
+ ipInput.disabled = false;
554
+ netmaskSelect.disabled = false;
555
+ gatewayInput.disabled = false;
556
+ dnsInput.disabled = false;
557
+ ipInput.style.backgroundColor = '';
558
+ netmaskSelect.style.backgroundColor = '';
559
+ gatewayInput.style.backgroundColor = '';
560
+ dnsInput.style.backgroundColor = '';
561
+ dhcpRow.style.borderColor = '#e0e0e0';
562
+ dhcpRow.style.background = '';
563
+ }
564
+ }
565
+
566
+ // 加载网络配置
567
+ async function loadNetwork() {
568
+ try {
569
+ const res = await fetch('/api/network');
570
+ const data = await res.json();
571
+
572
+ const card = document.getElementById('network-status-card');
573
+ const statusIcon = document.getElementById('status-icon');
574
+ const statusTitle = document.getElementById('status-title');
575
+ const statusDetail = document.getElementById('status-detail');
576
+ const modeBadge = document.getElementById('mode-badge');
577
+
578
+ if (data.interfaces && data.interfaces.length > 0) {
579
+ const eth0 = data.interfaces.find(i => i.name === 'eth0');
580
+
581
+ if (eth0) {
582
+ const ip = eth0.ip || '未获取';
583
+ const gw = eth0.gateway || '未知';
584
+ const isDhcp = eth0.dhcp;
585
+
586
+ statusIcon.textContent = ip !== '未获取' ? '🌐' : '📡';
587
+ statusTitle.textContent = 'eth0 - 有线网卡已连接';
588
+
589
+ const cidrToMask = {
590
+ '8': '255.0.0.0', '16': '255.255.0.0', '24': '255.255.255.0',
591
+ '25': '255.255.255.128', '26': '255.255.255.192', '27': '255.255.255.224',
592
+ '28': '255.255.255.240', '29': '255.255.255.248', '30': '255.255.255.252'
593
+ };
594
+ const maskDisplay = cidrToMask[eth0.netmask] || eth0.netmask;
595
+ statusDetail.textContent = 'IP: ' + ip + ' | 掩码: ' + maskDisplay + ' | 网关: ' + gw;
596
+
597
+ if (isDhcp) {
598
+ card.style.background = 'linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%)';
599
+ card.style.border = '2px solid #28a745';
600
+ modeBadge.textContent = '🔄 DHCP 动态';
601
+ modeBadge.style.background = '#28a745';
602
+ modeBadge.style.color = 'white';
603
+ } else {
604
+ card.style.background = 'linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%)';
605
+ card.style.border = '2px solid #ffc107';
606
+ modeBadge.textContent = '📌 静态 IP';
607
+ modeBadge.style.background = '#e6a800';
608
+ modeBadge.style.color = 'white';
609
+ }
610
+
611
+ document.getElementById('ip').value = eth0.ip || '';
612
+ const netmaskSelect = document.getElementById('netmask');
613
+ for (let i = 0; i < netmaskSelect.options.length; i++) {
614
+ if (netmaskSelect.options[i].value === (eth0.netmask || '24')) {
615
+ netmaskSelect.selectedIndex = i;
616
+ break;
617
+ }
618
+ }
619
+ document.getElementById('gateway').value = eth0.gateway || '';
620
+ document.getElementById('use-dhcp').checked = isDhcp || false;
621
+ toggleDhcp(isDhcp || false);
622
+ } else {
623
+ statusIcon.textContent = '⚠️';
624
+ statusTitle.textContent = 'eth0 未找到';
625
+ statusDetail.textContent = '请检查有线网卡连接';
626
+ card.style.background = '#f8d7da';
627
+ card.style.border = '2px solid #dc3545';
628
+ modeBadge.textContent = '❌ 未连接';
629
+ modeBadge.style.background = '#dc3545';
630
+ modeBadge.style.color = 'white';
631
+ }
632
+ } else {
633
+ statusIcon.textContent = '❌';
634
+ statusTitle.textContent = '未检测到网络接口';
635
+ statusDetail.textContent = '--';
636
+ card.style.background = '#f8d7da';
637
+ card.style.border = '2px solid #dc3545';
638
+ modeBadge.textContent = '错误';
639
+ modeBadge.style.background = '#dc3545';
640
+ modeBadge.style.color = 'white';
641
+ }
642
+ } catch (e) {
643
+ showMessage('network', 'error', '加载网络信息失败: ' + e.message);
644
+ }
645
+ }
646
+
647
+ // 保存网络配置(保存并立即应用)
648
+ document.getElementById('network-form').addEventListener('submit', async (e) => {
649
+ e.preventDefault();
650
+
651
+ if (!confirm('保存将立即应用网络配置并重启相关服务,可能导致连接中断!确定继续?')) return;
652
+
653
+ const useDhcp = document.getElementById('use-dhcp').checked;
654
+
655
+ const data = {
656
+ interface: 'eth0',
657
+ ip: document.getElementById('ip').value,
658
+ netmask: document.getElementById('netmask').value,
659
+ gateway: document.getElementById('gateway').value,
660
+ dns: document.getElementById('dns').value,
661
+ useDhcp: useDhcp,
662
+ applyNow: true
663
+ };
664
+
665
+ try {
666
+ const res = await fetch('/api/network', {
667
+ method: 'POST',
668
+ headers: {'Content-Type': 'application/json'},
669
+ body: JSON.stringify(data)
670
+ });
671
+ const result = await res.json();
672
+
673
+ if (result.success) {
674
+ showMessage('network', 'success', result.message);
675
+ } else {
676
+ showMessage('network', 'error', result.error);
677
+ }
678
+ } catch (e) {
679
+ showMessage('network', 'error', '保存失败: ' + e.message);
680
+ }
681
+ });
682
+
683
+ // 加载应用配置
684
+ async function loadConfig() {
685
+ const container = document.getElementById('config-container');
686
+ container.innerHTML = '<div class="loading">正在加载配置...</div>';
687
+
688
+ try {
689
+ const res = await fetch('/api/config');
690
+ const data = await res.json();
691
+
692
+ if (data.config) {
693
+ let html = '';
694
+ for (const [section, values] of Object.entries(data.config)) {
695
+ html += `<div class="config-section">
696
+ <h3>[${section}]</h3>
697
+ <div class="config-grid">`;
698
+
699
+ for (const [key, value] of Object.entries(values)) {
700
+ const inputHtml = generateInputHtml(section, key, value);
701
+ html += `
702
+ <div class="config-item">
703
+ <label>${key}</label>
704
+ ${inputHtml}
705
+ </div>`;
706
+ }
707
+
708
+ html += `</div></div>`;
709
+ }
710
+ container.innerHTML = html;
711
+ }
712
+ } catch (e) {
713
+ showMessage('config', 'error', '加载配置失败: ' + e.message);
714
+ }
715
+ }
716
+
717
+ // 根据值类型生成对应的输入控件
718
+ function generateInputHtml(section, key, value) {
719
+ const strValue = value || '';
720
+
721
+ // 布尔值 -> 复选框
722
+ if (strValue.toLowerCase() === 'true' || strValue.toLowerCase() === 'false') {
723
+ const checked = strValue.toLowerCase() === 'true' ? 'checked' : '';
724
+ return `<div class="checkbox-wrapper">
725
+ <input type="checkbox"
726
+ data-section="${section}"
727
+ data-key="${key}"
728
+ data-type="boolean"
729
+ ${checked}
730
+ onchange="this.nextElementSibling.textContent = this.checked">
731
+ <span class="checkbox-label">${strValue.toLowerCase() === 'true' ? 'true' : 'false'}</span>
732
+ </div>`;
733
+ }
734
+
735
+ // 端口号 -> 数字输入框 + 验证
736
+ if (key.toLowerCase().includes('port') || key.toLowerCase().endsWith('port')) {
737
+ return `<input type="number"
738
+ data-section="${section}"
739
+ data-key="${key}"
740
+ data-type="port"
741
+ value="${strValue}"
742
+ min="1"
743
+ max="65535"
744
+ oninput="validatePort(this)">`;
745
+ }
746
+
747
+ // IP 地址 -> 文本框 + 验证
748
+ if (/^(ip|bindip|listenip|serverip|remoteip|localip|hostip)$/i.test(key)) {
749
+ return `<input type="text"
750
+ data-section="${section}"
751
+ data-key="${key}"
752
+ data-type="ip"
753
+ value="${strValue}"
754
+ oninput="validateIp(this)"
755
+ placeholder="例如: 192.168.1.1">`;
756
+ }
757
+
758
+ // 纯数字 -> 数字输入框
759
+ if (/^\d+$/.test(strValue) && strValue !== '') {
760
+ return `<input type="number"
761
+ data-section="${section}"
762
+ data-key="${key}"
763
+ data-type="number"
764
+ value="${strValue}"
765
+ oninput="validateNumber(this)">`;
766
+ }
767
+
768
+ // 默认 -> 文本框
769
+ return `<input type="text"
770
+ data-section="${section}"
771
+ data-key="${key}"
772
+ data-type="text"
773
+ value="${strValue}">`;
774
+ }
775
+
776
+ // 验证端口号
777
+ function validatePort(input) {
778
+ const value = parseInt(input.value);
779
+ const isValid = value >= 1 && value <= 65535;
780
+
781
+ input.classList.toggle('invalid', !isValid);
782
+
783
+ let errorMsg = input.nextElementSibling;
784
+ if (!isValid && input.value !== '') {
785
+ if (!errorMsg || !errorMsg.classList.contains('validation-error')) {
786
+ errorMsg = document.createElement('div');
787
+ errorMsg.className = 'validation-error';
788
+ errorMsg.textContent = '端口范围: 1-65535';
789
+ input.parentNode.insertBefore(errorMsg, input.nextSibling);
790
+ }
791
+ } else if (errorMsg && errorMsg.classList.contains('validation-error')) {
792
+ errorMsg.remove();
793
+ }
794
+
795
+ return isValid;
796
+ }
797
+
798
+ // 验证 IP 地址
799
+ function validateIp(input) {
800
+ const value = input.value.trim();
801
+
802
+ // 允许空值
803
+ if (value === '') {
804
+ input.classList.remove('invalid');
805
+ removeValidationError(input);
806
+ return true;
807
+ }
808
+
809
+ // IP 正则: x.x.x.x 格式,每个段 0-255
810
+ const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
811
+ const match = value.match(ipRegex);
812
+
813
+ let isValid = false;
814
+ if (match) {
815
+ const nums = [match[1], match[2], match[3], match[4]].map(Number);
816
+ isValid = nums.every(n => n >= 0 && n <= 255);
817
+ }
818
+
819
+ input.classList.toggle('invalid', !isValid);
820
+
821
+ let errorMsg = input.nextElementSibling;
822
+ if (!isValid) {
823
+ if (!errorMsg || !errorMsg.classList.contains('validation-error')) {
824
+ errorMsg = document.createElement('div');
825
+ errorMsg.className = 'validation-error';
826
+ errorMsg.textContent = 'IP 格式无效 (例如: 192.168.1.1)';
827
+ input.parentNode.insertBefore(errorMsg, input.nextSibling);
828
+ }
829
+ } else if (errorMsg && errorMsg.classList.contains('validation-error')) {
830
+ errorMsg.remove();
831
+ }
832
+
833
+ return isValid;
834
+ }
835
+
836
+ // 验证纯数字
837
+ function validateNumber(input) {
838
+ const isValid = /^\d*$/.test(input.value);
839
+ input.classList.toggle('invalid', !isValid);
840
+
841
+ let errorMsg = input.nextElementSibling;
842
+ if (!isValid) {
843
+ if (!errorMsg || !errorMsg.classList.contains('validation-error')) {
844
+ errorMsg = document.createElement('div');
845
+ errorMsg.className = 'validation-error';
846
+ errorMsg.textContent = '只能输入数字';
847
+ input.parentNode.insertBefore(errorMsg, input.nextSibling);
848
+ }
849
+ } else if (errorMsg && errorMsg.classList.contains('validation-error')) {
850
+ errorMsg.remove();
851
+ }
852
+
853
+ return isValid;
854
+ }
855
+
856
+ // 移除验证错误提示
857
+ function removeValidationError(input) {
858
+ const errorMsg = input.nextElementSibling;
859
+ if (errorMsg && errorMsg.classList.contains('validation-error')) {
860
+ errorMsg.remove();
861
+ }
862
+ }
863
+
864
+ // 保存应用配置
865
+ async function saveConfig() {
866
+ const inputs = document.querySelectorAll('#config-container input[data-section]');
867
+ const config = {};
868
+ let hasErrors = false;
869
+
870
+ inputs.forEach(input => {
871
+ const section = input.dataset.section;
872
+ const key = input.dataset.key;
873
+ const type = input.dataset.type;
874
+
875
+ if (!config[section]) config[section] = {};
876
+
877
+ let value;
878
+
879
+ if (type === 'boolean') {
880
+ // 复选框 -> true/false
881
+ value = input.checked ? 'true' : 'false';
882
+ } else if (type === 'port') {
883
+ // 验证端口
884
+ if (!validatePort(input)) {
885
+ hasErrors = true;
886
+ return;
887
+ }
888
+ value = input.value;
889
+ } else if (type === 'ip') {
890
+ // 验证 IP
891
+ if (!validateIp(input)) {
892
+ hasErrors = true;
893
+ return;
894
+ }
895
+ value = input.value.trim();
896
+ } else if (type === 'number') {
897
+ // 验证数字
898
+ if (!validateNumber(input)) {
899
+ hasErrors = true;
900
+ return;
901
+ }
902
+ value = input.value;
903
+ } else {
904
+ value = input.value;
905
+ }
906
+
907
+ config[section][key] = value;
908
+ });
909
+
910
+ if (hasErrors) {
911
+ showMessage('config', 'error', '请修正输入错误后再保存');
912
+ return;
913
+ }
914
+
915
+ try {
916
+ const res = await fetch('/api/config', {
917
+ method: 'POST',
918
+ headers: {'Content-Type': 'application/json'},
919
+ body: JSON.stringify({config})
920
+ });
921
+ const result = await res.json();
922
+
923
+ if (result.success) {
924
+ showMessage('config', 'success', result.message);
925
+ } else {
926
+ showMessage('config', 'error', result.error);
927
+ }
928
+ } catch (e) {
929
+ showMessage('config', 'error', '保存失败: ' + e.message);
930
+ }
931
+ }
932
+
933
+ // 加载 QQBot 配置
934
+ // QQBot 当前启用状态
935
+ let qqbotEnabled = true;
936
+
937
+ async function loadQQBot() {
938
+ try {
939
+ const res = await fetch('/api/qqbot');
940
+ const data = await res.json();
941
+
942
+ if (data.config) {
943
+ document.getElementById('qq-uin').value = data.config.uin || '';
944
+ document.getElementById('qq-password').value = data.config.password || '';
945
+ qqbotEnabled = data.config.enabled !== false;
946
+ }
947
+ updateQQBotUI();
948
+ } catch (e) {
949
+ showMessage('qqbot', 'error', '加载配置失败: ' + e.message);
950
+ }
951
+ }
952
+
953
+ function updateQQBotUI() {
954
+ const card = document.getElementById('qqbot-status-card');
955
+ const icon = document.getElementById('qqbot-status-icon');
956
+ const text = document.getElementById('qqbot-status-text');
957
+ const btn = document.getElementById('qqbot-toggle-btn');
958
+
959
+ if (qqbotEnabled) {
960
+ card.style.background = 'linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%)';
961
+ card.style.border = '2px solid #28a745';
962
+ icon.textContent = '🟢';
963
+ text.textContent = 'QQBot 已启用,正在运行中';
964
+ btn.textContent = '🔴 禁用 QQBot';
965
+ btn.style.background = '#dc3545';
966
+ btn.style.color = 'white';
967
+ } else {
968
+ card.style.background = 'linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%)';
969
+ card.style.border = '2px solid #dc3545';
970
+ icon.textContent = '🔴';
971
+ text.textContent = 'QQBot 已禁用';
972
+ btn.textContent = '🟢 启用 QQBot';
973
+ btn.style.background = '#28a745';
974
+ btn.style.color = 'white';
975
+ }
976
+ }
977
+
978
+ async function toggleQQBot() {
979
+ const newState = !qqbotEnabled;
980
+ const action = newState ? '启用' : '禁用';
981
+ if (!confirm('确定要' + action + ' QQBot 吗?需要重启 Gateway 才能生效。')) return;
982
+
983
+ try {
984
+ const res = await fetch('/api/qqbot-toggle', {
985
+ method: 'POST',
986
+ headers: {'Content-Type': 'application/json'},
987
+ body: JSON.stringify({enabled: newState})
988
+ });
989
+ const result = await res.json();
990
+ if (result.success) {
991
+ qqbotEnabled = newState;
992
+ updateQQBotUI();
993
+ showMessage('qqbot', 'success', result.message);
994
+ } else {
995
+ showMessage('qqbot', 'error', result.error);
996
+ }
997
+ } catch (e) {
998
+ showMessage('qqbot', 'error', action + '失败: ' + e.message);
999
+ }
1000
+ }
1001
+
1002
+ // 保存 QQBot 配置
1003
+ document.getElementById('qqbot-form').addEventListener('submit', async (e) => {
1004
+ e.preventDefault();
1005
+
1006
+ const data = {
1007
+ uin: document.getElementById('qq-uin').value,
1008
+ password: document.getElementById('qq-password').value
1009
+ };
1010
+
1011
+ try {
1012
+ const res = await fetch('/api/qqbot', {
1013
+ method: 'POST',
1014
+ headers: {'Content-Type': 'application/json'},
1015
+ body: JSON.stringify(data)
1016
+ });
1017
+ const result = await res.json();
1018
+
1019
+ if (result.success) {
1020
+ showMessage('qqbot', 'success', result.message);
1021
+ } else {
1022
+ showMessage('qqbot', 'error', result.error);
1023
+ }
1024
+ } catch (e) {
1025
+ showMessage('qqbot', 'error', '保存失败: ' + e.message);
1026
+ }
1027
+ });
1028
+
1029
+ // 重启小播鼠服务
1030
+ async function restartAdoreMix() {
1031
+ if (!confirm('确定要重启小播鼠服务吗?')) return;
1032
+
1033
+ try {
1034
+ const res = await fetch('/api/adoremix-restart', {method: 'POST'});
1035
+ const result = await res.json();
1036
+
1037
+ if (result.success) {
1038
+ showMessage('config', 'success', result.message);
1039
+ } else {
1040
+ showMessage('config', 'error', result.error || '重启失败');
1041
+ }
1042
+ } catch (e) {
1043
+ showMessage('config', 'error', '重启失败: ' + e.message);
1044
+ }
1045
+ }
1046
+
1047
+ // 重启 OpenClaw Gateway
1048
+ async function restartOpenClaw() {
1049
+ if (!confirm('确定要重启 OpenClaw Gateway 吗?')) return;
1050
+
1051
+ try {
1052
+ const res = await fetch('/api/openclaw-restart', {method: 'POST'});
1053
+ const result = await res.json();
1054
+
1055
+ if (result.success) {
1056
+ showMessage('qqbot', 'success', result.message);
1057
+ showMessage('models', 'success', result.message);
1058
+ } else {
1059
+ showMessage('qqbot', 'error', result.error);
1060
+ showMessage('models', 'error', result.error);
1061
+ }
1062
+ } catch (e) {
1063
+ showMessage('qqbot', 'error', '重启失败: ' + e.message);
1064
+ showMessage('models', 'error', '重启失败: ' + e.message);
1065
+ }
1066
+ }
1067
+
1068
+ // 页面加载时初始化
1069
+ loadNetwork();
1070
+
1071
+ // 提供商配置映射
1072
+ const providerConfig = {
1073
+ 'openai': { keyModel: 'openai/gpt-4o', defaultUrl: 'https://api.openai.com/v1' },
1074
+ 'anthropic': { keyModel: 'anthropic/claude-3-5-sonnet', defaultUrl: 'https://api.anthropic.com' },
1075
+ 'google': { keyModel: 'google/gemini-1.5-pro', defaultUrl: 'https://generativelanguage.googleapis.com' },
1076
+ 'zhipu': { keyModel: 'zai/glm-4', defaultUrl: 'https://open.bigmodel.cn/api/paas/v4' },
1077
+ 'deepseek': { keyModel: 'deepseek/deepseek-chat', defaultUrl: 'https://api.deepseek.com' },
1078
+ 'moonshot': { keyModel: 'moonshot/moonshot-v1-8k', defaultUrl: 'https://api.moonshot.cn/v1' },
1079
+ 'qwen': { keyModel: 'qwen/qwen-turbo', defaultUrl: 'https://dashscope.aliyuncs.com/api/v1' },
1080
+ 'ollama': { keyModel: 'ollama/llama3', defaultUrl: 'http://localhost:11434' }
1081
+ };
1082
+
1083
+ // 提供商前缀映射
1084
+ const providerKeyMap = {
1085
+ 'openai': ['openai/', 'gpt-', 'o1-'],
1086
+ 'anthropic': ['anthropic/', 'claude-'],
1087
+ 'google': ['google/', 'gemini-'],
1088
+ 'zhipu': ['zai/', 'glm-'],
1089
+ 'deepseek': ['deepseek/'],
1090
+ 'moonshot': ['moonshot/', 'kimi-'],
1091
+ 'qwen': ['qwen/', 'tongyi-'],
1092
+ 'ollama': ['ollama/']
1093
+ };
1094
+
1095
+ // 加载模型配置
1096
+ async function loadModels() {
1097
+ try {
1098
+ const res = await fetch('/api/models');
1099
+ const data = await res.json();
1100
+
1101
+ // 填充模型选择
1102
+ const select = document.getElementById('primary-model');
1103
+ select.innerHTML = '';
1104
+
1105
+ const providers = {};
1106
+ data.availableModels.forEach(model => {
1107
+ if (!providers[model.provider]) {
1108
+ providers[model.provider] = [];
1109
+ }
1110
+ providers[model.provider].push(model);
1111
+ });
1112
+
1113
+ for (const [provider, models] of Object.entries(providers)) {
1114
+ const group = document.createElement('optgroup');
1115
+ group.label = provider.toUpperCase();
1116
+
1117
+ models.forEach(model => {
1118
+ const option = document.createElement('option');
1119
+ option.value = model.id;
1120
+ // 标注当前模型
1121
+ if (model.id === data.primary) {
1122
+ option.textContent = model.name + ' ✓ 当前';
1123
+ option.selected = true;
1124
+ } else {
1125
+ option.textContent = model.name;
1126
+ }
1127
+ group.appendChild(option);
1128
+ });
1129
+
1130
+ select.appendChild(group);
1131
+ }
1132
+
1133
+ // 添加自定义选项
1134
+ const customOption = document.createElement('option');
1135
+ customOption.value = '__custom__';
1136
+ customOption.textContent = '自定义模型 ID...';
1137
+ select.appendChild(customOption);
1138
+
1139
+ // 如果当前模型不在列表中,添加为自定义
1140
+ if (data.primary && !data.availableModels.find(m => m.id === data.primary)) {
1141
+ const customGroup = document.createElement('optgroup');
1142
+ customGroup.label = '当前';
1143
+ const currentOption = document.createElement('option');
1144
+ currentOption.value = data.primary;
1145
+ currentOption.textContent = data.primary + ' (当前)';
1146
+ currentOption.selected = true;
1147
+ customGroup.appendChild(currentOption);
1148
+ select.insertBefore(customGroup, select.firstChild);
1149
+ }
1150
+
1151
+ // 填充 API keys 和 URLs
1152
+ if (data.models) {
1153
+ for (const [modelId, cfg] of Object.entries(data.models)) {
1154
+ if (!cfg) continue;
1155
+
1156
+ // 根据模型 ID 判断提供商
1157
+ for (const [provider, prefixes] of Object.entries(providerKeyMap)) {
1158
+ if (prefixes.some(p => modelId.startsWith(p) || modelId.includes(p))) {
1159
+ // API Key
1160
+ if (cfg.apiKey) {
1161
+ const keyInput = document.getElementById(provider + '-key');
1162
+ if (keyInput) {
1163
+ keyInput.value = cfg.apiKey;
1164
+ }
1165
+ }
1166
+ // Base URL
1167
+ if (cfg.baseUrl) {
1168
+ const urlInput = document.getElementById(provider + '-url');
1169
+ if (urlInput) {
1170
+ urlInput.value = cfg.baseUrl;
1171
+ }
1172
+ }
1173
+ break;
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+
1179
+ } catch (e) {
1180
+ showMessage('models', 'error', '加载模型配置失败: ' + e.message);
1181
+ }
1182
+ }
1183
+
1184
+ // 保存模型配置
1185
+ async function saveModels() {
1186
+ const primary = document.getElementById('primary-model').value;
1187
+
1188
+ if (!primary || primary === '__custom__') {
1189
+ showMessage('models', 'error', '请选择一个有效的模型');
1190
+ return;
1191
+ }
1192
+
1193
+ // 收集配置
1194
+ const modelConfigs = {};
1195
+
1196
+ for (const [provider, config] of Object.entries(providerConfig)) {
1197
+ const keyInput = document.getElementById(provider + '-key');
1198
+ const urlInput = document.getElementById(provider + '-url');
1199
+
1200
+ const apiKey = keyInput ? keyInput.value.trim() : '';
1201
+ const baseUrl = urlInput ? urlInput.value.trim() : '';
1202
+
1203
+ if (apiKey || baseUrl) {
1204
+ modelConfigs[config.keyModel] = {};
1205
+ if (apiKey) {
1206
+ modelConfigs[config.keyModel].apiKey = apiKey;
1207
+ }
1208
+ if (baseUrl) {
1209
+ modelConfigs[config.keyModel].baseUrl = baseUrl;
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ try {
1215
+ const res = await fetch('/api/models', {
1216
+ method: 'POST',
1217
+ headers: {'Content-Type': 'application/json'},
1218
+ body: JSON.stringify({
1219
+ primary: primary,
1220
+ modelConfigs: modelConfigs
1221
+ })
1222
+ });
1223
+ const result = await res.json();
1224
+
1225
+ if (result.success) {
1226
+ showMessage('models', 'success', result.message);
1227
+ } else {
1228
+ showMessage('models', 'error', result.error);
1229
+ }
1230
+ } catch (e) {
1231
+ showMessage('models', 'error', '保存失败: ' + e.message);
1232
+ }
1233
+ }
1234
+
1235
+ // ==================== 实时日志 ====================
1236
+ let logPollingTimer = null;
1237
+ let lastLogLines = [];
1238
+ let lastLogService = '';
1239
+
1240
+ function escapeHtml(s) {
1241
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1242
+ }
1243
+
1244
+ function colorizeLogLine(line) {
1245
+ const lower = line.toLowerCase();
1246
+ if (/(error|failed|exit code|exit er|er[0-9]\b)/i.test(line)) {
1247
+ return '#f48771';
1248
+ } else if (/(warning|warn|deprecated)/i.test(line)) {
1249
+ return '#dcdcaa';
1250
+ } else if (/(started|success|completed|loaded|active|listening|connected)/i.test(lower)) {
1251
+ return '#4ec9b0';
1252
+ } else if (/(stopped|deactivated|inactive|dead|disconnected)/i.test(lower)) {
1253
+ return '#9cdcfe';
1254
+ }
1255
+ return '#d4d4d4';
1256
+ }
1257
+
1258
+ async function fetchLogs() {
1259
+ const service = document.getElementById('log-service').value;
1260
+ const lines = document.getElementById('log-lines').value;
1261
+ const since = document.getElementById('log-since').value;
1262
+ const status = document.getElementById('log-status');
1263
+
1264
+ if (lastLogService && lastLogService !== service) {
1265
+ lastLogLines = [];
1266
+ document.getElementById('log-output').innerHTML = '<div style="color: #888;">加载中...</div>';
1267
+ }
1268
+ lastLogService = service;
1269
+
1270
+ status.textContent = '⏳ 加载中…';
1271
+ status.style.color = '#667eea';
1272
+
1273
+ try {
1274
+ const res = await fetch(`/api/logs?service=${encodeURIComponent(service)}&lines=${lines}&since=${encodeURIComponent(since)}`);
1275
+ const data = await res.json();
1276
+
1277
+ if (data.error) {
1278
+ status.textContent = '❌ ' + data.error;
1279
+ status.style.color = '#dc3545';
1280
+ showMessage('logs', 'error', data.error);
1281
+ return;
1282
+ }
1283
+
1284
+ lastLogLines = data.lines || [];
1285
+ renderLogs();
1286
+
1287
+ const now = new Date();
1288
+ const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false });
1289
+ const sinceLabel = document.querySelector('#log-since option:checked').textContent;
1290
+ const sourceTag = data.source === 'file' ? `📁 ${data.path || ''}` : '📡 journalctl';
1291
+ status.textContent = `✅ ${data.count} 行 · ${sinceLabel} · ${sourceTag} · 更新于 ${timeStr}`;
1292
+ status.style.color = '#28a745';
1293
+ if (data.warning) {
1294
+ showMessage('logs', 'error', data.warning);
1295
+ }
1296
+ } catch (e) {
1297
+ status.textContent = '❌ ' + e.message;
1298
+ status.style.color = '#dc3545';
1299
+ }
1300
+ }
1301
+
1302
+ function renderLogs() {
1303
+ const output = document.getElementById('log-output');
1304
+ const filter = document.getElementById('log-filter').value;
1305
+ const autoScroll = document.getElementById('log-auto-scroll').checked;
1306
+
1307
+ const wasAtBottom = output.scrollTop + output.clientHeight >= output.scrollHeight - 20;
1308
+
1309
+ let html = '';
1310
+ if (lastLogLines.length === 0) {
1311
+ html = '<div style="color: #888;">(暂无日志)</div>';
1312
+ } else {
1313
+ const filterLower = filter.toLowerCase();
1314
+ const filterEscaped = filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1315
+ const re = filter ? new RegExp(filterEscaped, 'gi') : null;
1316
+ let visible = 0;
1317
+
1318
+ for (const line of lastLogLines) {
1319
+ if (filter && !line.toLowerCase().includes(filterLower)) continue;
1320
+ visible++;
1321
+ const color = colorizeLogLine(line);
1322
+ let display = escapeHtml(line);
1323
+ if (re) {
1324
+ display = display.replace(re, m => `<mark style="background:#ffc107;color:#000;padding:0 2px;border-radius:2px;">${m}</mark>`);
1325
+ }
1326
+ html += `<div style="color: ${color};">${display}</div>`;
1327
+ }
1328
+ if (visible === 0) {
1329
+ html = '<div style="color: #888;">(无匹配 “' + escapeHtml(filter) + '” 的日志行)</div>';
1330
+ }
1331
+ }
1332
+
1333
+ output.innerHTML = html;
1334
+ if (autoScroll || wasAtBottom) {
1335
+ output.scrollTop = output.scrollHeight;
1336
+ }
1337
+ }
1338
+
1339
+ function restartLogPolling() {
1340
+ if (logPollingTimer) {
1341
+ clearInterval(logPollingTimer);
1342
+ logPollingTimer = null;
1343
+ }
1344
+ const autoRefresh = document.getElementById('log-auto-refresh');
1345
+ const interval = parseInt(document.getElementById('log-interval').value);
1346
+ if (autoRefresh.checked && interval > 0) {
1347
+ logPollingTimer = setInterval(fetchLogs, interval);
1348
+ }
1349
+ fetchLogs();
1350
+ }
1351
+
1352
+ function copyLogs() {
1353
+ const text = lastLogLines.join('\n');
1354
+ if (!text) { showMessage('logs', 'error', '没有可复制的日志'); return; }
1355
+ if (navigator.clipboard && window.isSecureContext) {
1356
+ navigator.clipboard.writeText(text).then(() => {
1357
+ showMessage('logs', 'success', `已复制 ${lastLogLines.length} 行到剪贴板`);
1358
+ }).catch(e => showMessage('logs', 'error', '复制失败: ' + e.message));
1359
+ } else {
1360
+ const ta = document.createElement('textarea');
1361
+ ta.value = text;
1362
+ document.body.appendChild(ta);
1363
+ ta.select();
1364
+ try { document.execCommand('copy'); showMessage('logs', 'success', `已复制 ${lastLogLines.length} 行`); }
1365
+ catch (e) { showMessage('logs', 'error', '复制失败: ' + e.message); }
1366
+ document.body.removeChild(ta);
1367
+ }
1368
+ }
1369
+
1370
+ function downloadLogs() {
1371
+ const text = lastLogLines.join('\n');
1372
+ if (!text) { showMessage('logs', 'error', '没有可下载的日志'); return; }
1373
+ const service = document.getElementById('log-service').value;
1374
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
1375
+ const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
1376
+ const url = URL.createObjectURL(blob);
1377
+ const a = document.createElement('a');
1378
+ a.href = url;
1379
+ a.download = `${service}-${ts}.log`;
1380
+ document.body.appendChild(a);
1381
+ a.click();
1382
+ document.body.removeChild(a);
1383
+ URL.revokeObjectURL(url);
1384
+ }
1385
+
1386
+ function clearLogDisplay() {
1387
+ if (!confirm('清空当前显示的日志(不影响服务器上的真实日志)?')) return;
1388
+ lastLogLines = [];
1389
+ renderLogs();
1390
+ const status = document.getElementById('log-status');
1391
+ status.textContent = '已清空显示';
1392
+ status.style.color = '#888';
1393
+ }
1394
+ </script>
1395
+ </body>
1396
+ </html>