nonebot-plugin-shiro-web-console 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of nonebot-plugin-shiro-web-console might be problematic. Click here for more details.
- nonebot_plugin_shiro_web_console/__init__.py +835 -0
- nonebot_plugin_shiro_web_console/static/index.html +1505 -0
- nonebot_plugin_shiro_web_console-0.1.0.dist-info/METADATA +59 -0
- nonebot_plugin_shiro_web_console-0.1.0.dist-info/RECORD +6 -0
- nonebot_plugin_shiro_web_console-0.1.0.dist-info/WHEEL +4 -0
- nonebot_plugin_shiro_web_console-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1505 @@
|
|
|
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>NoneBot Web 控制台</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--primary-color: #0078d4;
|
|
10
|
+
--bg-main: #f3f2f1;
|
|
11
|
+
--bg-sidebar: #201f1e;
|
|
12
|
+
--bg-view: #ffffff;
|
|
13
|
+
--bg-card: #ffffff;
|
|
14
|
+
--bg-input: #ffffff;
|
|
15
|
+
--text-main: #323130;
|
|
16
|
+
--text-secondary: #605e5c;
|
|
17
|
+
--text-on-primary: #ffffff;
|
|
18
|
+
--border-color: #edebe9;
|
|
19
|
+
--nav-item-hover: #323130;
|
|
20
|
+
--msg-received-bg: #ffffff;
|
|
21
|
+
--msg-sent-bg: #0078d4;
|
|
22
|
+
--shadow: 0 2px 10px rgba(0,0,0,0.05);
|
|
23
|
+
--nav-width: 60px;
|
|
24
|
+
--sidebar-width: 300px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[data-theme="dark"] {
|
|
28
|
+
--bg-main: #0a0a0a;
|
|
29
|
+
--bg-sidebar: #050505;
|
|
30
|
+
--bg-view: #121212;
|
|
31
|
+
--bg-card: #1e1e1e;
|
|
32
|
+
--bg-input: #252525;
|
|
33
|
+
--text-main: #e0e0e0;
|
|
34
|
+
--text-secondary: #aaaaaa;
|
|
35
|
+
--border-color: #333333;
|
|
36
|
+
--nav-item-hover: #2a2a2a;
|
|
37
|
+
--msg-received-bg: #252525;
|
|
38
|
+
--msg-sent-bg: #0078d4;
|
|
39
|
+
--shadow: 0 4px 20px rgba(0,0,0,0.5);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
* { box-sizing: border-box; transition: background-color 0.2s, color 0.2s, border-color 0.2s; }
|
|
43
|
+
body, html {
|
|
44
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
45
|
+
margin: 0;
|
|
46
|
+
padding: 0;
|
|
47
|
+
background: var(--bg-main);
|
|
48
|
+
color: var(--text-main);
|
|
49
|
+
height: 100vh;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#app { display: flex; width: 100vw; height: 100vh; filter: blur(5px); pointer-events: none; }
|
|
54
|
+
#app.authenticated { filter: none; pointer-events: auto; }
|
|
55
|
+
|
|
56
|
+
/* 导航栏 */
|
|
57
|
+
#nav-bar {
|
|
58
|
+
width: var(--nav-width);
|
|
59
|
+
background: var(--bg-sidebar);
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
align-items: center;
|
|
63
|
+
padding: 20px 0;
|
|
64
|
+
gap: 20px;
|
|
65
|
+
z-index: 200;
|
|
66
|
+
}
|
|
67
|
+
.nav-item {
|
|
68
|
+
width: 40px;
|
|
69
|
+
height: 40px;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
color: #adadad;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
transition: all 0.2s;
|
|
77
|
+
font-size: 20px;
|
|
78
|
+
}
|
|
79
|
+
.nav-item:hover { background: var(--nav-item-hover); color: white; }
|
|
80
|
+
.nav-item.active { background: var(--nav-item-hover); color: var(--primary-color); }
|
|
81
|
+
|
|
82
|
+
/* 内容容器 */
|
|
83
|
+
#content-wrapper { flex: 1; display: flex; overflow: hidden; position: relative; }
|
|
84
|
+
.page { display: none; width: 100%; height: 100%; }
|
|
85
|
+
.page.active { display: flex; }
|
|
86
|
+
|
|
87
|
+
/* 登录遮罩 */
|
|
88
|
+
#login-mask {
|
|
89
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
90
|
+
background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center;
|
|
91
|
+
z-index: 1000; transition: opacity 0.3s;
|
|
92
|
+
}
|
|
93
|
+
#login-mask.hidden { opacity: 0; pointer-events: none; }
|
|
94
|
+
#login-box {
|
|
95
|
+
background: var(--bg-card); padding: 30px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
96
|
+
width: 380px; text-align: center; display: flex; flex-direction: column; gap: 15px;
|
|
97
|
+
color: var(--text-main);
|
|
98
|
+
}
|
|
99
|
+
.login-tabs { display: flex; border-bottom: 1px solid var(--border-color); margin-bottom: 10px; }
|
|
100
|
+
.login-tab { flex: 1; padding: 10px; cursor: pointer; color: var(--text-secondary); font-size: 0.9em; transition: all 0.2s; }
|
|
101
|
+
.login-tab.active { color: var(--primary-color); border-bottom: 2px solid var(--primary-color); font-weight: bold; }
|
|
102
|
+
.login-input-group { display: flex; flex-direction: column; gap: 10px; text-align: left; }
|
|
103
|
+
.login-input-group input { padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; outline: none; width: 100%; background: var(--bg-input); color: var(--text-main); }
|
|
104
|
+
.login-input-group input:focus { border-color: var(--primary-color); }
|
|
105
|
+
|
|
106
|
+
/* 侧边栏 */
|
|
107
|
+
#sidebar {
|
|
108
|
+
width: var(--sidebar-width);
|
|
109
|
+
background: var(--bg-view);
|
|
110
|
+
border-right: 1px solid var(--border-color);
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
}
|
|
114
|
+
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border-color); font-weight: bold; font-size: 1.2em; background: var(--bg-view); color: var(--text-main); }
|
|
115
|
+
#chat-list { flex: 1; overflow-y: auto; background: var(--bg-view); }
|
|
116
|
+
.chat-item {
|
|
117
|
+
display: flex; align-items: center; padding: 12px 20px; cursor: pointer;
|
|
118
|
+
transition: background 0.2s; border-bottom: 1px solid var(--border-color);
|
|
119
|
+
color: var(--text-main);
|
|
120
|
+
}
|
|
121
|
+
.chat-item:hover { background: var(--bg-main); }
|
|
122
|
+
.chat-item.active { background: var(--bg-main); border-left: 4px solid var(--primary-color); }
|
|
123
|
+
.avatar { width: 36px; height: 36px; border-radius: 50%; margin-right: 12px; background: var(--border-color); flex-shrink: 0; }
|
|
124
|
+
|
|
125
|
+
/* 主内容区 */
|
|
126
|
+
.main-view { flex: 1; display: flex; flex-direction: column; background: var(--bg-view); color: var(--text-main); }
|
|
127
|
+
#chat-header, .page-header {
|
|
128
|
+
padding: 10px 15px; border-bottom: 1px solid var(--border-color);
|
|
129
|
+
display: flex; align-items: center; justify-content: flex-start;
|
|
130
|
+
min-height: 60px; background: var(--bg-view); gap: 10px;
|
|
131
|
+
}
|
|
132
|
+
.back-btn {
|
|
133
|
+
display: none; width: 32px; height: 32px; align-items: center; justify-content: center;
|
|
134
|
+
border-radius: 50%; cursor: pointer; color: var(--text-main); font-size: 1.2em;
|
|
135
|
+
transition: background 0.2s;
|
|
136
|
+
}
|
|
137
|
+
.back-btn:hover { background: var(--bg-main); }
|
|
138
|
+
#messages, .page-content { flex: 1; padding: 20px; overflow-y: auto; background: var(--bg-main); }
|
|
139
|
+
#messages { display: flex; flex-direction: column; gap: 15px; }
|
|
140
|
+
|
|
141
|
+
/* 插件页样式 */
|
|
142
|
+
.plugin-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
|
143
|
+
.plugin-card {
|
|
144
|
+
background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px;
|
|
145
|
+
padding: 20px; display: flex; flex-direction: column; gap: 12px;
|
|
146
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); color: var(--text-main);
|
|
147
|
+
position: relative; overflow: hidden;
|
|
148
|
+
}
|
|
149
|
+
.plugin-card:hover { transform: translateY(-4px); box-shadow: var(--shadow); border-color: var(--primary-color); }
|
|
150
|
+
.plugin-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
|
|
151
|
+
.plugin-name { font-weight: bold; font-size: 1.1em; color: var(--text-main); flex: 1; }
|
|
152
|
+
.plugin-version { font-size: 0.75em; color: var(--primary-color); background: rgba(0, 120, 212, 0.1); padding: 2px 8px; border-radius: 20px; font-weight: 500; }
|
|
153
|
+
.plugin-desc { font-size: 0.9em; color: var(--text-secondary); line-height: 1.5; height: 2.8em; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
154
|
+
|
|
155
|
+
.plugin-footer { display: flex; justify-content: space-between; align-items: center; margin-top: auto; padding-top: 10px; border-top: 1px solid var(--border-color); }
|
|
156
|
+
.plugin-actions { display: flex; gap: 8px; }
|
|
157
|
+
.btn-icon {
|
|
158
|
+
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
|
159
|
+
border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-main);
|
|
160
|
+
color: var(--text-secondary); cursor: pointer; text-decoration: none; transition: all 0.2s;
|
|
161
|
+
}
|
|
162
|
+
.btn-icon:hover { background: var(--primary-color); color: white; border-color: var(--primary-color); }
|
|
163
|
+
.btn-config {
|
|
164
|
+
padding: 6px 16px; border-radius: 6px; border: 1px solid var(--primary-color);
|
|
165
|
+
background: transparent; color: var(--primary-color); cursor: pointer; font-weight: 500;
|
|
166
|
+
transition: all 0.2s;
|
|
167
|
+
}
|
|
168
|
+
.btn-config:hover { background: var(--primary-color); color: white; }
|
|
169
|
+
|
|
170
|
+
/* 插件标签 */
|
|
171
|
+
.plugin-tag { font-size: 0.7em; padding: 2px 8px; border-radius: 20px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
172
|
+
.tag-official { background: rgba(255, 152, 0, 0.15); color: #ef6c00; }
|
|
173
|
+
.tag-builtin { background: rgba(76, 175, 80, 0.15); color: #2e7d32; }
|
|
174
|
+
.tag-store { background: rgba(33, 150, 243, 0.15); color: #1565c0; }
|
|
175
|
+
.tag-local { background: rgba(158, 158, 158, 0.15); color: #616161; }
|
|
176
|
+
.tag-valid { background: rgba(40, 167, 69, 0.15); color: #28a745; }
|
|
177
|
+
.tag-invalid { background: rgba(220, 53, 69, 0.15); color: #dc3545; }
|
|
178
|
+
[data-theme="dark"] .tag-official { background: rgba(255, 152, 0, 0.2); color: #ffb74d; }
|
|
179
|
+
[data-theme="dark"] .tag-builtin { background: rgba(76, 175, 80, 0.2); color: #81c784; }
|
|
180
|
+
[data-theme="dark"] .tag-store { background: rgba(33, 150, 243, 0.2); color: #64b5f6; }
|
|
181
|
+
[data-theme="dark"] .tag-local { background: rgba(158, 158, 158, 0.2); color: #bdbdbd; }
|
|
182
|
+
[data-theme="dark"] .tag-valid { background: rgba(40, 167, 69, 0.2); color: #81c784; }
|
|
183
|
+
[data-theme="dark"] .tag-invalid { background: rgba(220, 53, 69, 0.2); color: #f87171; }
|
|
184
|
+
|
|
185
|
+
/* 插件分组样式 */
|
|
186
|
+
.plugin-category-group { margin-bottom: 30px; }
|
|
187
|
+
.plugin-category-title {
|
|
188
|
+
font-size: 1.1em;
|
|
189
|
+
font-weight: bold;
|
|
190
|
+
margin-bottom: 15px;
|
|
191
|
+
padding-bottom: 8px;
|
|
192
|
+
border-bottom: 2px solid var(--border-color);
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: 10px;
|
|
196
|
+
color: var(--text-main);
|
|
197
|
+
}
|
|
198
|
+
.plugin-category-count {
|
|
199
|
+
font-size: 0.75em;
|
|
200
|
+
background: var(--border-color);
|
|
201
|
+
padding: 2px 8px;
|
|
202
|
+
border-radius: 10px;
|
|
203
|
+
color: var(--text-secondary);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* 商店卡片样式 */
|
|
207
|
+
.store-card {
|
|
208
|
+
background: var(--bg-card);
|
|
209
|
+
padding: 20px;
|
|
210
|
+
border-radius: 12px;
|
|
211
|
+
border: 1px solid var(--border-color);
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: 12px;
|
|
215
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
216
|
+
}
|
|
217
|
+
.store-card:hover { transform: translateY(-4px); box-shadow: var(--shadow); border-color: var(--primary-color); }
|
|
218
|
+
.store-name { font-weight: bold; font-size: 1.1em; color: var(--primary-color); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
219
|
+
.store-name:hover { text-decoration: underline; }
|
|
220
|
+
.store-desc { font-size: 0.9em; color: var(--text-secondary); line-height: 1.5; flex: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
221
|
+
.store-meta { font-size: 0.75em; color: var(--text-secondary); display: flex; flex-wrap: wrap; gap: 10px; padding: 8px 0; }
|
|
222
|
+
.store-meta span { display: flex; align-items: center; gap: 4px; }
|
|
223
|
+
.store-footer { display: flex; justify-content: space-between; align-items: center; margin-top: auto; padding-top: 10px; border-top: 1px solid var(--border-color); }
|
|
224
|
+
.store-btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 0.85em; font-weight: 600; transition: all 0.2s; }
|
|
225
|
+
.store-btn-install { background: var(--primary-color); color: white; }
|
|
226
|
+
.store-btn-update { background: rgba(40, 167, 69, 0.15); color: #28a745; border: 1px solid #28a745; }
|
|
227
|
+
.store-btn-update:hover { background: #28a745; color: white; }
|
|
228
|
+
.store-btn-uninstall { background: rgba(220, 53, 69, 0.15); color: #dc3545; border: 1px solid #dc3545; }
|
|
229
|
+
.store-btn-uninstall:hover { background: #dc3545; color: white; }
|
|
230
|
+
.store-btn:disabled { background: var(--bg-main); color: var(--text-secondary); border: 1px solid var(--border-color); cursor: not-allowed; }
|
|
231
|
+
|
|
232
|
+
/* 配置弹窗 */
|
|
233
|
+
#config-modal {
|
|
234
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
235
|
+
background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center;
|
|
236
|
+
z-index: 1100;
|
|
237
|
+
}
|
|
238
|
+
#config-box {
|
|
239
|
+
background: var(--bg-card); width: 600px; max-width: 90%; max-height: 80vh;
|
|
240
|
+
border-radius: 8px; display: flex; flex-direction: column; color: var(--text-main);
|
|
241
|
+
}
|
|
242
|
+
.modal-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; font-weight: bold; }
|
|
243
|
+
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
|
|
244
|
+
.modal-footer { padding: 15px 20px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 10px; }
|
|
245
|
+
.config-item { margin-bottom: 15px; }
|
|
246
|
+
.config-item label { display: block; margin-bottom: 5px; font-weight: 500; font-size: 0.9em; }
|
|
247
|
+
.config-item input, .config-item select, .config-item textarea { width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-input); color: var(--text-main); }
|
|
248
|
+
|
|
249
|
+
/* 消息样式升级 */
|
|
250
|
+
.message-wrapper { display: flex; width: 100%; gap: 10px; }
|
|
251
|
+
.message-wrapper.sent { flex-direction: row-reverse; }
|
|
252
|
+
.msg-avatar { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; background: var(--border-color); }
|
|
253
|
+
.message { max-width: 70%; padding: 10px 15px; border-radius: 8px; line-height: 1.5; position: relative; word-break: break-all; box-shadow: var(--shadow); }
|
|
254
|
+
.message img {
|
|
255
|
+
max-width: 100%;
|
|
256
|
+
max-height: 400px;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
display: block;
|
|
259
|
+
margin: 5px 0;
|
|
260
|
+
cursor: zoom-in;
|
|
261
|
+
object-fit: contain;
|
|
262
|
+
}
|
|
263
|
+
.message.received { background: var(--msg-received-bg); color: var(--text-main); border: 1px solid var(--border-color); }
|
|
264
|
+
.message.sent { background: var(--msg-sent-bg); color: white; }
|
|
265
|
+
.msg-meta { font-size: 0.75em; margin-bottom: 4px; color: var(--text-secondary); }
|
|
266
|
+
|
|
267
|
+
/* 输入框 */
|
|
268
|
+
#input-area { padding: 20px; border-top: 1px solid var(--border-color); display: flex; gap: 10px; background: var(--bg-view); }
|
|
269
|
+
#msg-input { flex: 1; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; outline: none; background: var(--bg-input); color: var(--text-main); }
|
|
270
|
+
#msg-input:focus { border-color: var(--primary-color); }
|
|
271
|
+
#send-btn { padding: 10px 25px; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
272
|
+
#send-btn:hover { opacity: 0.9; }
|
|
273
|
+
#send-btn:disabled { background: #ccc; cursor: not-allowed; }
|
|
274
|
+
|
|
275
|
+
.tag { font-size: 0.7em; padding: 2px 6px; border-radius: 10px; margin-left: 5px; vertical-align: middle; }
|
|
276
|
+
.tag-group { background: #e1f5fe; color: #01579b; }
|
|
277
|
+
.tag-private { background: #f3e5f5; color: #4a148c; }
|
|
278
|
+
|
|
279
|
+
.sub-nav {
|
|
280
|
+
display: flex;
|
|
281
|
+
background: var(--bg-view);
|
|
282
|
+
padding: 0 20px;
|
|
283
|
+
border-bottom: 1px solid var(--border-color);
|
|
284
|
+
gap: 20px;
|
|
285
|
+
}
|
|
286
|
+
.sub-nav-item {
|
|
287
|
+
padding: 12px 10px;
|
|
288
|
+
font-size: 0.9em;
|
|
289
|
+
color: var(--text-secondary);
|
|
290
|
+
cursor: pointer;
|
|
291
|
+
position: relative;
|
|
292
|
+
font-weight: 500;
|
|
293
|
+
transition: all 0.2s;
|
|
294
|
+
}
|
|
295
|
+
.sub-nav-item:hover { color: var(--primary-color); }
|
|
296
|
+
.sub-nav-item.active { color: var(--primary-color); }
|
|
297
|
+
.sub-nav-item.active::after {
|
|
298
|
+
content: "";
|
|
299
|
+
position: absolute;
|
|
300
|
+
bottom: 0;
|
|
301
|
+
left: 0;
|
|
302
|
+
right: 0;
|
|
303
|
+
height: 3px;
|
|
304
|
+
background: var(--primary-color);
|
|
305
|
+
border-radius: 3px 3px 0 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* 移动端适配 */
|
|
309
|
+
@media (max-width: 768px) {
|
|
310
|
+
:root {
|
|
311
|
+
--nav-width: 100%;
|
|
312
|
+
--nav-height: 60px;
|
|
313
|
+
--sidebar-width: 100%;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#app { flex-direction: column; }
|
|
317
|
+
|
|
318
|
+
#nav-bar {
|
|
319
|
+
width: 100%;
|
|
320
|
+
height: var(--nav-height);
|
|
321
|
+
flex-direction: row;
|
|
322
|
+
order: 2; /* 放到下面 */
|
|
323
|
+
padding: 0;
|
|
324
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
325
|
+
justify-content: space-around;
|
|
326
|
+
border-top: 1px solid var(--border-color);
|
|
327
|
+
background: var(--bg-view);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.nav-item { flex: 1; height: 100%; border-radius: 0; font-size: 24px; }
|
|
331
|
+
|
|
332
|
+
#content-wrapper { order: 1; height: calc(100vh - var(--nav-height)); }
|
|
333
|
+
|
|
334
|
+
#sidebar {
|
|
335
|
+
position: absolute; left: 0; top: 0; width: 100%; height: 100%;
|
|
336
|
+
z-index: 100; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
337
|
+
border-right: none; background: var(--bg-view);
|
|
338
|
+
}
|
|
339
|
+
#sidebar.hidden { transform: translateX(-100%); }
|
|
340
|
+
#sidebar.hidden ~ .main-view .back-btn { display: flex; }
|
|
341
|
+
|
|
342
|
+
.main-view { width: 100%; height: 100%; flex: 1; }
|
|
343
|
+
.message { max-width: 85%; }
|
|
344
|
+
.message img { max-height: 300px; }
|
|
345
|
+
#login-box { width: 90%; padding: 25px; }
|
|
346
|
+
|
|
347
|
+
/* 仪表盘卡片调整 */
|
|
348
|
+
.page-content { padding: 15px; }
|
|
349
|
+
.dashboard-grid { grid-template-columns: 1fr !important; }
|
|
350
|
+
|
|
351
|
+
/* 插件列表调整 */
|
|
352
|
+
.plugin-list { grid-template-columns: 1fr !important; gap: 15px; }
|
|
353
|
+
|
|
354
|
+
/* 弹窗调整 */
|
|
355
|
+
#config-box { width: 95%; max-height: 90vh; }
|
|
356
|
+
#store-box { width: 95% !important; height: 90vh !important; }
|
|
357
|
+
|
|
358
|
+
/* 防止 iOS 输入框自动缩放 */
|
|
359
|
+
input, select, textarea { font-size: 16px !important; }
|
|
360
|
+
|
|
361
|
+
.modal-body { padding: 15px; }
|
|
362
|
+
.config-item { margin-bottom: 20px; }
|
|
363
|
+
}
|
|
364
|
+
</style>
|
|
365
|
+
</head>
|
|
366
|
+
<body>
|
|
367
|
+
<div id="login-mask">
|
|
368
|
+
<div id="login-box">
|
|
369
|
+
<h2 style="margin: 0;">控制台登录</h2>
|
|
370
|
+
<div class="login-tabs">
|
|
371
|
+
<div class="login-tab active" onclick="switchLoginTab('code')">验证码登录</div>
|
|
372
|
+
<div class="login-tab" onclick="switchLoginTab('password')">密码登录</div>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div id="login-code-section" class="login-input-group">
|
|
376
|
+
<p style="font-size: 0.85em; color: #666; margin: 0;">验证码将发送至管理员 QQ</p>
|
|
377
|
+
<div style="display: flex; gap: 10px;">
|
|
378
|
+
<input type="text" id="code-input" placeholder="6 位验证码" maxlength="6">
|
|
379
|
+
<button id="send-code-btn" style="padding: 10px; background: #0078d4; color: white; border: none; border-radius: 6px; cursor: pointer; min-width: 100px;">获取</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div id="login-password-section" class="login-input-group" style="display: none;">
|
|
384
|
+
<p style="font-size: 0.85em; color: #666; margin: 0;">请输入管理员密码</p>
|
|
385
|
+
<input type="password" id="password-input" placeholder="管理员密码">
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<button id="login-btn" style="width: 100%; padding: 12px; background: #28a745; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 1.1em; font-weight: bold; margin-top: 10px;">立即登录</button>
|
|
389
|
+
<div id="login-msg" style="font-size: 0.9em; min-height: 1.2em;"></div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div id="app">
|
|
393
|
+
<div id="nav-bar">
|
|
394
|
+
<div class="nav-item active" onclick="showPage('home')" title="首页">🏠</div>
|
|
395
|
+
<div class="nav-item" onclick="showPage('chat')" title="聊天">💬</div>
|
|
396
|
+
<div class="nav-item" onclick="showPage('plugins')" title="插件管理">🧩</div>
|
|
397
|
+
<div class="nav-item" onclick="showPage('logs')" title="日志">📜</div>
|
|
398
|
+
<div class="nav-item" onclick="showPage('settings')" title="设置">⚙️</div>
|
|
399
|
+
</div>
|
|
400
|
+
<div id="content-wrapper">
|
|
401
|
+
<!-- 首页/仪表盘 -->
|
|
402
|
+
<div id="page-home" class="page active">
|
|
403
|
+
<div class="main-view">
|
|
404
|
+
<div class="page-header">
|
|
405
|
+
<div style="font-weight: bold; font-size: 1.2em;">仪表盘</div>
|
|
406
|
+
<div style="display: flex; align-items: center; gap: 15px;">
|
|
407
|
+
<button id="theme-toggle-btn" onclick="toggleThemeManual()" style="background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); padding: 5px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 0.9em; transition: all 0.2s;">
|
|
408
|
+
<span id="theme-icon">🌙</span> <span id="theme-text">深色模式</span>
|
|
409
|
+
</button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
<div class="page-content">
|
|
413
|
+
<div class="dashboard-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
|
|
414
|
+
<!-- 机器人状态卡片 -->
|
|
415
|
+
<div style="background: var(--bg-card); padding: 25px; border-radius: 12px; border: 1px solid var(--border-color); box-shadow: var(--shadow); display: flex; flex-direction: column;">
|
|
416
|
+
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
|
|
417
|
+
<h3 style="margin: 0; display: flex; align-items: center; gap: 8px;">🤖 机器人状态</h3>
|
|
418
|
+
<div style="display: flex; gap: 8px;">
|
|
419
|
+
<button onclick="handleBotAction('reboot')" class="btn-icon" title="重启 Bot" style="color: #ffc107; border-color: #ffc107; background: rgba(255, 193, 7, 0.1); font-size: 1.2em; font-weight: bold; line-height: 1;">↻</button>
|
|
420
|
+
<button onclick="handleBotAction('shutdown')" class="btn-icon" title="关闭 Bot" style="color: #dc3545; border-color: #dc3545; background: rgba(220, 53, 69, 0.1); font-size: 1.2em; font-weight: bold; line-height: 1;">⏻</button>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
<div id="bot-status-container" style="flex: 1;">
|
|
424
|
+
<div style="text-align: center; color: #999; padding: 20px;">正在获取机器人状态...</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<!-- 设备运行状态 -->
|
|
429
|
+
<div style="background: var(--bg-card); padding: 25px; border-radius: 12px; border: 1px solid var(--border-color); box-shadow: var(--shadow);">
|
|
430
|
+
<h3 style="margin-top: 0; display: flex; align-items: center; gap: 8px;">💻 设备运行情况</h3>
|
|
431
|
+
<div id="system-status-container">
|
|
432
|
+
<div class="status-item" style="margin-bottom: 15px;">
|
|
433
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
434
|
+
<span>CPU 使用率</span>
|
|
435
|
+
<span id="cpu-val">-</span>
|
|
436
|
+
</div>
|
|
437
|
+
<div style="height: 8px; background: var(--border-color); border-radius: 4px; overflow: hidden;">
|
|
438
|
+
<div id="cpu-bar" style="height: 100%; width: 0%; background: var(--primary-color); transition: width 0.5s;"></div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="status-item" style="margin-bottom: 15px;">
|
|
442
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
443
|
+
<span>内存使用 (RAM)</span>
|
|
444
|
+
<span id="mem-val">-</span>
|
|
445
|
+
</div>
|
|
446
|
+
<div style="height: 8px; background: var(--border-color); border-radius: 4px; overflow: hidden;">
|
|
447
|
+
<div id="mem-bar" style="height: 100%; width: 0%; background: #28a745; transition: width 0.5s;"></div>
|
|
448
|
+
</div>
|
|
449
|
+
<div id="mem-details" style="font-size: 0.8em; color: var(--text-secondary); margin-top: 8px;">-</div>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="status-item">
|
|
452
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
453
|
+
<span>磁盘使用 (DISK)</span>
|
|
454
|
+
<span id="disk-val">-</span>
|
|
455
|
+
</div>
|
|
456
|
+
<div style="height: 8px; background: var(--border-color); border-radius: 4px; overflow: hidden;">
|
|
457
|
+
<div id="disk-bar" style="height: 100%; width: 0%; background: #ffc107; transition: width 0.5s;"></div>
|
|
458
|
+
</div>
|
|
459
|
+
<div id="disk-details" style="font-size: 0.8em; color: var(--text-secondary); margin-top: 8px;">-</div>
|
|
460
|
+
</div>
|
|
461
|
+
<hr style="border: none; border-top: 1px solid var(--border-color); margin: 20px 0;">
|
|
462
|
+
<div style="font-size: 0.9em; color: var(--text-secondary); display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
463
|
+
<div id="sys-uptime" style="grid-column: 1 / -1;">运行时间: -</div>
|
|
464
|
+
<div id="sys-net-sent">发送流量: -</div>
|
|
465
|
+
<div id="sys-net-recv">接收流量: -</div>
|
|
466
|
+
<div id="sys-os">操作系统: -</div>
|
|
467
|
+
<div id="sys-py">Python: -</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<!-- 聊天页面 -->
|
|
477
|
+
<div id="page-chat" class="page">
|
|
478
|
+
<div id="sidebar" class="hidden">
|
|
479
|
+
<div class="sidebar-header">消息列表</div>
|
|
480
|
+
<div id="chat-list">
|
|
481
|
+
<div style="padding: 20px; text-align: center; color: var(--text-secondary);">加载中...</div>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div id="main" class="main-view">
|
|
485
|
+
<div id="chat-header">
|
|
486
|
+
<div class="header-left">
|
|
487
|
+
<div class="back-btn" onclick="toggleSidebar(true)">←</div>
|
|
488
|
+
<div id="current-chat-name">请选择一个聊天</div>
|
|
489
|
+
</div>
|
|
490
|
+
<div id="status" style="font-size: 0.85em; flex-shrink: 0;">● 已连接</div>
|
|
491
|
+
</div>
|
|
492
|
+
<div id="messages">
|
|
493
|
+
<div style="margin: auto; color: var(--text-secondary);">选择左侧列表开始聊天</div>
|
|
494
|
+
</div>
|
|
495
|
+
<div id="input-area" style="background: var(--bg-view); border-top: 1px solid var(--border-color);">
|
|
496
|
+
<input type="text" id="msg-input" placeholder="输入消息..." disabled style="background: var(--bg-input); color: var(--text-main); border: 1px solid var(--border-color);">
|
|
497
|
+
<button id="send-btn" disabled>发送</button>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<!-- 插件页面 -->
|
|
503
|
+
<div id="page-plugins" class="page">
|
|
504
|
+
<div class="main-view">
|
|
505
|
+
<div class="page-header" style="flex-direction: column; align-items: stretch; height: auto; padding: 0;">
|
|
506
|
+
<div style="display: flex; align-items: center; justify-content: space-between; padding: 15px 25px; border-bottom: 1px solid var(--border-color);">
|
|
507
|
+
<div style="font-weight: bold; font-size: 1.2em;">插件管理</div>
|
|
508
|
+
<div id="plugin-search-container" style="flex: 1; max-width: 400px; margin-left: 20px;">
|
|
509
|
+
<input type="text" id="local-plugin-search" placeholder="查找已安装插件..." style="width: 100%; padding: 6px 15px; border: 1px solid var(--border-color); border-radius: 20px; background: var(--bg-input); color: var(--text-main); font-size: 0.85em; outline: none;" oninput="filterLocalPlugins()">
|
|
510
|
+
<input type="text" id="store-search" placeholder="在商店中搜索插件..." style="width: 100%; padding: 6px 15px; border: 1px solid var(--border-color); border-radius: 20px; background: var(--bg-input); color: var(--text-main); font-size: 0.85em; outline: none; display: none;" oninput="filterStore()">
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="sub-nav">
|
|
514
|
+
<div class="sub-nav-item active" id="tab-local" onclick="switchPluginTab('local')">已安装</div>
|
|
515
|
+
<div class="sub-nav-item" id="tab-store" onclick="switchPluginTab('store')">插件商店</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="page-content" id="plugin-content" style="padding: 20px; background: var(--bg-main);">
|
|
519
|
+
<!-- 本地插件视图 -->
|
|
520
|
+
<div id="local-plugins-view">
|
|
521
|
+
<div id="plugin-list">
|
|
522
|
+
<!-- 插件卡片将在此处分组渲染 -->
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
<!-- 商店视图 -->
|
|
526
|
+
<div id="store-plugins-view" style="display: none;">
|
|
527
|
+
<div id="store-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px;">
|
|
528
|
+
<!-- 商店插件卡片 -->
|
|
529
|
+
</div>
|
|
530
|
+
<div id="store-status-bar" style="margin-top: 20px; padding: 10px 0; font-size: 0.85em; color: var(--text-secondary); border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center;">
|
|
531
|
+
<span id="store-count">正在加载商店数据...</span>
|
|
532
|
+
<span id="action-status" style="font-weight: bold; color: var(--primary-color);"></span>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<!-- 日志页面 -->
|
|
540
|
+
<div id="page-logs" class="page">
|
|
541
|
+
<div class="main-view">
|
|
542
|
+
<div class="page-header">
|
|
543
|
+
<div style="font-weight: bold; font-size: 1.2em;">NoneBot 日志</div>
|
|
544
|
+
<div style="display: flex; gap: 10px;">
|
|
545
|
+
<button onclick="fetchLogs()" style="padding: 5px 12px; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em;">刷新</button>
|
|
546
|
+
<button onclick="document.getElementById('log-viewer').innerHTML = ''" style="padding: 5px 12px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em;">清屏</button>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="page-content" style="background: #1e1e1e; padding: 0; overflow: hidden; display: flex; flex-direction: column;">
|
|
550
|
+
<div id="log-viewer" style="flex: 1; padding: 15px; overflow-y: auto; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; line-height: 1.6; color: #d4d4d4;">
|
|
551
|
+
<div style="color: #888;">正在加载日志...</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<!-- 设置页面 -->
|
|
558
|
+
<div id="page-settings" class="page">
|
|
559
|
+
<div class="main-view">
|
|
560
|
+
<div class="page-header">
|
|
561
|
+
<div style="font-weight: bold; font-size: 1.2em;">系统设置</div>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="page-content">
|
|
564
|
+
<div class="settings-group">
|
|
565
|
+
<div class="settings-title">关于</div>
|
|
566
|
+
<div class="setting-item">
|
|
567
|
+
<div>
|
|
568
|
+
<div style="font-weight: 500;">NoneBot Web Console</div>
|
|
569
|
+
<div style="font-size: 0.85em; color: var(--text-secondary);">版本 v1.2.0</div>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<!-- 配置弹窗 -->
|
|
580
|
+
<div id="config-modal">
|
|
581
|
+
<div id="config-box">
|
|
582
|
+
<div class="modal-header">
|
|
583
|
+
<span id="modal-title">插件配置</span>
|
|
584
|
+
<span style="cursor: pointer;" onclick="closeConfig()">✕</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div id="modal-body" class="modal-body">
|
|
587
|
+
<!-- 配置项表单将在此处生成 -->
|
|
588
|
+
</div>
|
|
589
|
+
<div class="modal-footer">
|
|
590
|
+
<button onclick="closeConfig()" style="padding: 8px 15px; border: 1px solid var(--border-color); background: var(--bg-card); color: var(--text-main); border-radius: 4px; cursor: pointer;">取消</button>
|
|
591
|
+
<button id="save-config-btn" style="padding: 8px 15px; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer;">保存配置</button>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<script>
|
|
597
|
+
let currentChatId = null;
|
|
598
|
+
let ws = null;
|
|
599
|
+
let authToken = localStorage.getItem('web_console_token');
|
|
600
|
+
let currentEditingPlugin = null;
|
|
601
|
+
|
|
602
|
+
const appEl = document.getElementById('app');
|
|
603
|
+
const loginMask = document.getElementById('login-mask');
|
|
604
|
+
const codeInput = document.getElementById('code-input');
|
|
605
|
+
const sendCodeBtn = document.getElementById('send-code-btn');
|
|
606
|
+
const loginBtn = document.getElementById('login-btn');
|
|
607
|
+
const loginMsg = document.getElementById('login-msg');
|
|
608
|
+
const sidebar = document.getElementById('sidebar');
|
|
609
|
+
|
|
610
|
+
// 页面切换逻辑
|
|
611
|
+
function showPage(pageId) {
|
|
612
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
613
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
614
|
+
document.getElementById(`page-${pageId}`).classList.add('active');
|
|
615
|
+
|
|
616
|
+
// 更新导航图标激活状态
|
|
617
|
+
const navItems = document.querySelectorAll('.nav-item');
|
|
618
|
+
if (pageId === 'home') { navItems[0].classList.add('active'); fetchStatus(); }
|
|
619
|
+
else if (pageId === 'chat') {
|
|
620
|
+
navItems[1].classList.add('active');
|
|
621
|
+
// 移动端切换到聊天页时,如果没有选择聊天,则显示侧边栏
|
|
622
|
+
if (window.innerWidth <= 768 && !currentChatId) {
|
|
623
|
+
toggleSidebar(true);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else if (pageId === 'plugins') {
|
|
627
|
+
navItems[2].classList.add('active');
|
|
628
|
+
fetchPlugins();
|
|
629
|
+
}
|
|
630
|
+
else if (pageId === 'logs') { navItems[3].classList.add('active'); fetchLogs(); }
|
|
631
|
+
else if (pageId === 'settings') navItems[4].classList.add('active');
|
|
632
|
+
|
|
633
|
+
// 移动端切换到非聊天页时隐藏侧边栏
|
|
634
|
+
if (window.innerWidth <= 768 && pageId !== 'chat') {
|
|
635
|
+
toggleSidebar(false);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function switchPluginTab(tab) {
|
|
640
|
+
// 更新标签样式
|
|
641
|
+
document.querySelectorAll('.sub-nav-item').forEach(item => item.classList.remove('active'));
|
|
642
|
+
document.getElementById(`tab-${tab}`).classList.add('active');
|
|
643
|
+
|
|
644
|
+
// 切换视图
|
|
645
|
+
document.getElementById('local-plugins-view').style.display = tab === 'local' ? 'block' : 'none';
|
|
646
|
+
document.getElementById('store-plugins-view').style.display = tab === 'store' ? 'block' : 'none';
|
|
647
|
+
|
|
648
|
+
// 切换搜索框
|
|
649
|
+
document.getElementById('local-plugin-search').style.display = tab === 'local' ? 'block' : 'none';
|
|
650
|
+
document.getElementById('store-search').style.display = tab === 'store' ? 'block' : 'none';
|
|
651
|
+
|
|
652
|
+
if (tab === 'store') {
|
|
653
|
+
openStore();
|
|
654
|
+
} else {
|
|
655
|
+
fetchPlugins();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function fetchStatus() {
|
|
660
|
+
try {
|
|
661
|
+
const res = await authorizedFetch('/web_console/api/status');
|
|
662
|
+
const data = await res.json();
|
|
663
|
+
|
|
664
|
+
// 渲染机器人状态
|
|
665
|
+
const botContainer = document.getElementById('bot-status-container');
|
|
666
|
+
if (data.bots && data.bots.length > 0) {
|
|
667
|
+
botContainer.innerHTML = data.bots.map(bot => `
|
|
668
|
+
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 20px;">
|
|
669
|
+
<img src="${bot.avatar}" style="width: 60px; height: 60px; border-radius: 50%; border: 2px solid #eee;">
|
|
670
|
+
<div style="flex: 1;">
|
|
671
|
+
<div style="font-weight: bold; font-size: 1.1em;">${bot.nickname}</div>
|
|
672
|
+
<div style="color: #666; font-size: 0.9em;">ID: ${bot.id}</div>
|
|
673
|
+
</div>
|
|
674
|
+
<div style="padding: 4px 10px; border-radius: 20px; font-size: 0.85em; background: ${bot.status === '在线' ? '#e6f4ea' : '#fce8e6'}; color: ${bot.status === '在线' ? '#1e7e34' : '#c5221f'};">
|
|
675
|
+
${bot.status}
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
`).join('');
|
|
679
|
+
} else {
|
|
680
|
+
botContainer.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">未发现已连接的机器人</div>';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 渲染系统状态
|
|
684
|
+
document.getElementById('cpu-val').textContent = data.system.cpu;
|
|
685
|
+
document.getElementById('cpu-bar').style.width = data.system.cpu;
|
|
686
|
+
document.getElementById('mem-val').textContent = data.system.memory;
|
|
687
|
+
document.getElementById('mem-bar').style.width = data.system.memory;
|
|
688
|
+
document.getElementById('mem-details').textContent = `已用 ${data.system.memory_used} / 总共 ${data.system.memory_total}`;
|
|
689
|
+
|
|
690
|
+
// 新增磁盘状态更新
|
|
691
|
+
document.getElementById('disk-val').textContent = data.system.disk;
|
|
692
|
+
document.getElementById('disk-bar').style.width = data.system.disk;
|
|
693
|
+
document.getElementById('disk-details').textContent = `已用 ${data.system.disk_used} / 总共 ${data.system.disk_total}`;
|
|
694
|
+
|
|
695
|
+
// 新增系统详细信息更新
|
|
696
|
+
document.getElementById('sys-uptime').textContent = `运行时间: ${data.system.uptime}`;
|
|
697
|
+
document.getElementById('sys-net-sent').textContent = `发送流量: ${data.system.net_sent}`;
|
|
698
|
+
document.getElementById('sys-net-recv').textContent = `接收流量: ${data.system.net_recv}`;
|
|
699
|
+
document.getElementById('sys-os').textContent = `操作系统: ${data.system.os}`;
|
|
700
|
+
document.getElementById('sys-py').textContent = `Python: ${data.system.python}`;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
console.error('获取状态失败', e);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function handleBotAction(action) {
|
|
707
|
+
const actionText = action === 'reboot' ? '重启' : '关闭';
|
|
708
|
+
if (!confirm(`确定要${actionText}机器人吗?`)) return;
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const res = await authorizedFetch('/web_console/api/system/action', {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
body: JSON.stringify({ action })
|
|
714
|
+
});
|
|
715
|
+
const data = await res.json();
|
|
716
|
+
|
|
717
|
+
if (data.error) {
|
|
718
|
+
// 使用更友好的错误显示方式
|
|
719
|
+
const errorArea = document.createElement('div');
|
|
720
|
+
errorArea.style.cssText = 'position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:var(--bg-card); border:1px solid var(--border-color); padding:25px; border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,0.5); z-index:10001; max-width:90vw; width:500px;';
|
|
721
|
+
errorArea.innerHTML = `
|
|
722
|
+
<h3 style="margin-top:0; color:#dc3545;">❌ 操作失败</h3>
|
|
723
|
+
<div style="margin:15px 0; white-space:pre-wrap; font-size:14px; max-height:300px; overflow-y:auto; border-left:4px solid #dc3545; padding-left:15px;">${data.error}</div>
|
|
724
|
+
<button onclick="this.parentElement.remove()" class="btn-primary" style="width:100%;">确定</button>
|
|
725
|
+
`;
|
|
726
|
+
document.body.appendChild(errorArea);
|
|
727
|
+
} else {
|
|
728
|
+
alert(data.msg || '操作已发送');
|
|
729
|
+
if (action === 'shutdown') {
|
|
730
|
+
// 关闭后遮罩提示
|
|
731
|
+
document.body.innerHTML = '<div style="height: 100vh; display: flex; align-items: center; justify-content: center; background: #000; color: #fff; font-family: sans-serif;"><h1>Bot 已关闭</h1></div>';
|
|
732
|
+
} else {
|
|
733
|
+
// 重启后提示并刷新
|
|
734
|
+
setTimeout(() => window.location.reload(), 5000);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch (e) {
|
|
738
|
+
alert('发送请求失败,Bot 可能正在处理中...');
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function fetchLogs() {
|
|
743
|
+
const viewer = document.getElementById('log-viewer');
|
|
744
|
+
try {
|
|
745
|
+
const res = await authorizedFetch('/web_console/api/logs');
|
|
746
|
+
const logs = await res.json();
|
|
747
|
+
viewer.innerHTML = logs.map(log => {
|
|
748
|
+
let color = '#d4d4d4';
|
|
749
|
+
if (log.level === 'ERROR') color = '#f44747';
|
|
750
|
+
else if (log.level === 'WARNING') color = '#cca700';
|
|
751
|
+
else if (log.level === 'SUCCESS') color = '#6a9955';
|
|
752
|
+
else if (log.level === 'DEBUG') color = '#b5cea8';
|
|
753
|
+
|
|
754
|
+
return `<div style="margin-bottom: 4px;">
|
|
755
|
+
<span style="color: #888;">[${log.time}]</span>
|
|
756
|
+
<span style="color: ${color}; font-weight: bold; margin: 0 5px;">${log.level.padEnd(7)}</span>
|
|
757
|
+
<span style="color: #569cd6;">${log.module}</span>
|
|
758
|
+
<span style="color: #ce9178;"> - ${log.message}</span>
|
|
759
|
+
</div>`;
|
|
760
|
+
}).join('');
|
|
761
|
+
viewer.scrollTop = viewer.scrollHeight;
|
|
762
|
+
} catch (e) {
|
|
763
|
+
viewer.innerHTML = '<div style="color: #f44747;">获取日志失败</div>';
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 主题切换
|
|
768
|
+
function toggleTheme(isDark) {
|
|
769
|
+
const icon = document.getElementById('theme-icon');
|
|
770
|
+
const text = document.getElementById('theme-text');
|
|
771
|
+
|
|
772
|
+
if (isDark) {
|
|
773
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
774
|
+
localStorage.setItem('web-console-theme', 'dark');
|
|
775
|
+
if (icon) icon.textContent = '☀️';
|
|
776
|
+
if (text) text.textContent = '浅色模式';
|
|
777
|
+
} else {
|
|
778
|
+
document.documentElement.removeAttribute('data-theme');
|
|
779
|
+
localStorage.setItem('web-console-theme', 'light');
|
|
780
|
+
if (icon) icon.textContent = '🌙';
|
|
781
|
+
if (text) text.textContent = '深色模式';
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function toggleThemeManual() {
|
|
786
|
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
787
|
+
toggleTheme(!isDark);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function initTheme() {
|
|
791
|
+
const savedTheme = localStorage.getItem('web-console-theme');
|
|
792
|
+
const isDark = savedTheme === 'dark';
|
|
793
|
+
toggleTheme(isDark);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
let storeData = [];
|
|
797
|
+
let installedPluginModules = [];
|
|
798
|
+
|
|
799
|
+
async function openStore() {
|
|
800
|
+
// 获取已安装插件列表,用于判断状态
|
|
801
|
+
try {
|
|
802
|
+
const res = await authorizedFetch('/web_console/api/plugins');
|
|
803
|
+
const plugins = await res.json();
|
|
804
|
+
installedPluginModules = plugins.map(p => p.module);
|
|
805
|
+
} catch (e) {}
|
|
806
|
+
|
|
807
|
+
if (storeData.length === 0) {
|
|
808
|
+
try {
|
|
809
|
+
const res = await authorizedFetch('/web_console/api/store');
|
|
810
|
+
storeData = await res.json();
|
|
811
|
+
if (storeData.error) {
|
|
812
|
+
document.getElementById('store-list').innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #dc3545;">${storeData.error}</div>`;
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
} catch (e) {
|
|
816
|
+
document.getElementById('store-list').innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #dc3545;">获取商店数据失败</div>`;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
renderStore(storeData);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function renderStore(plugins) {
|
|
824
|
+
const listEl = document.getElementById('store-list');
|
|
825
|
+
const countEl = document.getElementById('store-count');
|
|
826
|
+
countEl.textContent = `共发现 ${plugins.length} 个插件`;
|
|
827
|
+
|
|
828
|
+
listEl.innerHTML = plugins.map(p => {
|
|
829
|
+
const isInstalled = installedPluginModules.includes(p.module_name);
|
|
830
|
+
const isValid = p.valid !== false; // 如果不存在 valid 字段,默认为 true
|
|
831
|
+
const isOfficial = p.is_official === true;
|
|
832
|
+
|
|
833
|
+
return `
|
|
834
|
+
<div class="store-card">
|
|
835
|
+
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px; min-width: 0;">
|
|
836
|
+
<a href="${p.homepage || '#'}" target="_blank" class="store-name" title="${p.name}">${p.name}</a>
|
|
837
|
+
<div style="display: flex; gap: 4px; flex-shrink: 0; align-items: center;">
|
|
838
|
+
${isOfficial ? `<span class="plugin-tag tag-official" style="font-size: 0.6em; padding: 1px 5px; line-height: 1.4;">官方</span>` : ''}
|
|
839
|
+
<span class="plugin-tag ${isValid ? 'tag-valid' : 'tag-invalid'}" style="font-size: 0.6em; padding: 1px 5px; line-height: 1.4;">
|
|
840
|
+
${isValid ? '已通过' : '未通过'}
|
|
841
|
+
</span>
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
<div class="store-desc" title="${p.desc}">${p.desc}</div>
|
|
845
|
+
<div class="store-meta">
|
|
846
|
+
<span>👤 ${p.author}</span>
|
|
847
|
+
<span>📦 ${p.version}</span>
|
|
848
|
+
</div>
|
|
849
|
+
<div class="store-footer">
|
|
850
|
+
<div style="display: flex; gap: 8px;">
|
|
851
|
+
${isInstalled ?
|
|
852
|
+
`<button onclick="handleStoreAction('update', '${p.project_link}')" class="store-btn store-btn-update">更新</button>
|
|
853
|
+
<button onclick="handleStoreAction('uninstall', '${p.project_link}')" class="store-btn store-btn-uninstall">卸载</button>` :
|
|
854
|
+
`<button onclick="handleStoreAction('install', '${p.project_link}')" class="store-btn store-btn-install" ${!isValid ? 'style="opacity: 0.6;"' : ''}>安装</button>`
|
|
855
|
+
}
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
`;
|
|
860
|
+
}).join('');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function filterStore() {
|
|
864
|
+
const keyword = document.getElementById('store-search').value.toLowerCase();
|
|
865
|
+
const filtered = storeData.filter(p => {
|
|
866
|
+
const name = p.name.toLowerCase();
|
|
867
|
+
const desc = p.desc.toLowerCase();
|
|
868
|
+
const author = p.author.toLowerCase();
|
|
869
|
+
const module = p.module_name.toLowerCase();
|
|
870
|
+
const isValid = p.valid !== false;
|
|
871
|
+
|
|
872
|
+
// 支持搜索 "已通过", "未通过", "官方"
|
|
873
|
+
const statusMatch = (keyword === '已通过' && isValid) ||
|
|
874
|
+
(keyword === '未通过' && !isValid) ||
|
|
875
|
+
(keyword === '官方' && p.is_official);
|
|
876
|
+
|
|
877
|
+
return name.includes(keyword) ||
|
|
878
|
+
desc.includes(keyword) ||
|
|
879
|
+
author.includes(keyword) ||
|
|
880
|
+
module.includes(keyword) ||
|
|
881
|
+
statusMatch;
|
|
882
|
+
});
|
|
883
|
+
renderStore(filtered);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function handleStoreAction(action, plugin) {
|
|
887
|
+
const statusEl = document.getElementById('action-status');
|
|
888
|
+
const btns = document.querySelectorAll('.store-btn');
|
|
889
|
+
|
|
890
|
+
if (!confirm(`确定要执行 ${action} 操作吗?`)) return;
|
|
891
|
+
|
|
892
|
+
btns.forEach(b => b.disabled = true);
|
|
893
|
+
statusEl.textContent = `正在 ${action}... 请稍候`;
|
|
894
|
+
|
|
895
|
+
try {
|
|
896
|
+
const res = await authorizedFetch('/web_console/api/store/action', {
|
|
897
|
+
method: 'POST',
|
|
898
|
+
headers: { 'Content-Type': 'application/json' },
|
|
899
|
+
body: JSON.stringify({ action, plugin })
|
|
900
|
+
});
|
|
901
|
+
const data = await res.json();
|
|
902
|
+
|
|
903
|
+
if (data.error) {
|
|
904
|
+
alert(`操作失败: ${data.error}`);
|
|
905
|
+
} else {
|
|
906
|
+
alert(`${data.msg}\n\n部分插件安装后需要手动在机器人配置中加载或重启生效。`);
|
|
907
|
+
// 刷新插件列表
|
|
908
|
+
fetchPlugins();
|
|
909
|
+
// 刷新商店显示状态
|
|
910
|
+
const res2 = await authorizedFetch('/web_console/api/plugins');
|
|
911
|
+
const plugins = await res2.json();
|
|
912
|
+
installedPluginModules = plugins.map(p => p.module);
|
|
913
|
+
filterStore();
|
|
914
|
+
}
|
|
915
|
+
} catch (e) {
|
|
916
|
+
alert('请求异常');
|
|
917
|
+
} finally {
|
|
918
|
+
btns.forEach(b => b.disabled = false);
|
|
919
|
+
statusEl.textContent = '';
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function fetchPlugins() {
|
|
924
|
+
const listEl = document.getElementById('plugin-list');
|
|
925
|
+
listEl.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: #999;">正在获取插件列表...</div>';
|
|
926
|
+
try {
|
|
927
|
+
const res = await authorizedFetch('/web_console/api/plugins');
|
|
928
|
+
const plugins = await res.json();
|
|
929
|
+
|
|
930
|
+
// 按来源分组
|
|
931
|
+
const groups = {
|
|
932
|
+
'official': { name: '官方插件', icon: '橙色', plugins: [] },
|
|
933
|
+
'builtin': { name: '内置插件', icon: '绿色', plugins: [] },
|
|
934
|
+
'store': { name: '商店插件', icon: '蓝色', plugins: [] },
|
|
935
|
+
'local': { name: '本地插件', icon: '灰色', plugins: [] }
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
plugins.forEach(p => {
|
|
939
|
+
const type = p.type || 'local';
|
|
940
|
+
if (groups[type]) {
|
|
941
|
+
groups[type].plugins.push(p);
|
|
942
|
+
} else {
|
|
943
|
+
groups['local'].plugins.push(p);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const typeLabels = {
|
|
948
|
+
'official': { text: '官方', class: 'tag-official' },
|
|
949
|
+
'builtin': { text: '内置', class: 'tag-builtin' },
|
|
950
|
+
'store': { text: '商店', class: 'tag-store' },
|
|
951
|
+
'local': { text: '本地', class: 'tag-local' }
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
let html = '';
|
|
955
|
+
const categoryOrder = ['official', 'builtin', 'store', 'local'];
|
|
956
|
+
|
|
957
|
+
categoryOrder.forEach(type => {
|
|
958
|
+
const group = groups[type];
|
|
959
|
+
if (group.plugins.length > 0) {
|
|
960
|
+
html += `
|
|
961
|
+
<div class="plugin-category-group" style="grid-column: 1/-1;">
|
|
962
|
+
<div class="plugin-category-title">
|
|
963
|
+
${group.name}
|
|
964
|
+
<span class="plugin-category-count">${group.plugins.length}</span>
|
|
965
|
+
</div>
|
|
966
|
+
<div class="plugin-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px;">
|
|
967
|
+
${group.plugins.map(p => {
|
|
968
|
+
const label = typeLabels[p.type] || typeLabels.local;
|
|
969
|
+
return `
|
|
970
|
+
<div class="plugin-card">
|
|
971
|
+
<div class="plugin-header">
|
|
972
|
+
<div class="plugin-name">${p.name || p.id}</div>
|
|
973
|
+
${p.version ? `<div class="plugin-version">${p.version}</div>` : ''}
|
|
974
|
+
</div>
|
|
975
|
+
<div class="plugin-desc">${p.description || '暂无描述'}</div>
|
|
976
|
+
<div class="plugin-footer">
|
|
977
|
+
<span class="plugin-tag ${label.class}">${label.text}</span>
|
|
978
|
+
<div class="plugin-actions">
|
|
979
|
+
${p.homepage ? `<a href="${p.homepage}" target="_blank" class="btn-icon" title="主页">🏠</a>` : ''}
|
|
980
|
+
<button onclick="handlePluginUninstall('${p.module}', '${p.name}')" class="btn-icon" title="卸载" style="color: #dc3545; border-color: rgba(220, 53, 69, 0.2);">🗑️</button>
|
|
981
|
+
<button onclick="openConfig('${p.id}')" class="btn-config">配置</button>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
`;
|
|
986
|
+
}).join('')}
|
|
987
|
+
</div>
|
|
988
|
+
</div>
|
|
989
|
+
`;
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
listEl.innerHTML = html || '<div style="grid-column: 1/-1; text-align: center; color: #999; padding: 20px;">未发现已加载的插件</div>';
|
|
994
|
+
|
|
995
|
+
// 如果搜索框有内容,自动应用过滤
|
|
996
|
+
if (document.getElementById('local-plugin-search').value) {
|
|
997
|
+
filterLocalPlugins();
|
|
998
|
+
}
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
listEl.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: #dc3545; padding: 20px;">获取插件列表失败</div>';
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function filterLocalPlugins() {
|
|
1005
|
+
const kw = document.getElementById('local-plugin-search').value.toLowerCase();
|
|
1006
|
+
const cards = document.querySelectorAll('#plugin-list .plugin-card');
|
|
1007
|
+
cards.forEach(card => {
|
|
1008
|
+
const title = card.querySelector('.plugin-name')?.innerText.toLowerCase() || "";
|
|
1009
|
+
const desc = card.querySelector('.plugin-desc')?.innerText.toLowerCase() || "";
|
|
1010
|
+
|
|
1011
|
+
const visible = title.includes(kw) || desc.includes(kw);
|
|
1012
|
+
card.style.display = visible ? 'flex' : 'none';
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// 处理分组标题的显示
|
|
1016
|
+
const groups = document.querySelectorAll('#plugin-list .plugin-category-group');
|
|
1017
|
+
groups.forEach(group => {
|
|
1018
|
+
const hasVisibleCards = Array.from(group.querySelectorAll('.plugin-card')).some(c => c.style.display !== 'none');
|
|
1019
|
+
group.style.display = hasVisibleCards ? 'block' : 'none';
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function handlePluginUninstall(module, name) {
|
|
1024
|
+
if (!confirm(`确定要卸载插件 "${name}" (${module}) 吗?\n卸载后可能需要重启机器人才能完全卸载。`)) return;
|
|
1025
|
+
|
|
1026
|
+
// 尝试通过模块名卸载,这是 nb-cli 的标准做法
|
|
1027
|
+
const pluginName = module.startsWith('nonebot_plugin_') ? module : module;
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
// 显示处理中状态
|
|
1031
|
+
const actionStatus = document.getElementById('action-status');
|
|
1032
|
+
if (actionStatus) actionStatus.textContent = `正在卸载 ${name}...`;
|
|
1033
|
+
|
|
1034
|
+
const res = await authorizedFetch('/web_console/api/store/action', {
|
|
1035
|
+
method: 'POST',
|
|
1036
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1037
|
+
body: JSON.stringify({ action: 'uninstall', plugin: pluginName })
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
const data = await res.json();
|
|
1041
|
+
if (data.error) {
|
|
1042
|
+
alert('卸载失败: ' + data.error);
|
|
1043
|
+
} else {
|
|
1044
|
+
alert(data.msg || '卸载成功,建议重启机器人以应用更改。');
|
|
1045
|
+
fetchPlugins(); // 刷新列表
|
|
1046
|
+
}
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
alert('请求失败');
|
|
1049
|
+
} finally {
|
|
1050
|
+
const actionStatus = document.getElementById('action-status');
|
|
1051
|
+
if (actionStatus) actionStatus.textContent = '';
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async function openConfig(pluginId) {
|
|
1056
|
+
currentEditingPlugin = pluginId;
|
|
1057
|
+
const modal = document.getElementById('config-modal');
|
|
1058
|
+
const body = document.getElementById('modal-body');
|
|
1059
|
+
document.getElementById('modal-title').textContent = `配置插件: ${pluginId}`;
|
|
1060
|
+
body.innerHTML = '<div style="text-align: center; color: #999;">正在加载配置项...</div>';
|
|
1061
|
+
modal.style.display = 'flex';
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
const res = await authorizedFetch(`/web_console/api/plugins/${pluginId}/config`);
|
|
1065
|
+
const data = await res.json();
|
|
1066
|
+
renderConfigForm(data.config, data.schema);
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
body.innerHTML = '<div style="text-align: center; color: red;">加载配置项失败</div>';
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function renderConfigForm(config, schema) {
|
|
1073
|
+
const body = document.getElementById('modal-body');
|
|
1074
|
+
body.innerHTML = '';
|
|
1075
|
+
|
|
1076
|
+
if (!schema || Object.keys(schema).length === 0) {
|
|
1077
|
+
body.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">该插件未导出配置项元数据</div>';
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
for (const key in schema) {
|
|
1082
|
+
const item = schema[key];
|
|
1083
|
+
const div = document.createElement('div');
|
|
1084
|
+
div.className = 'config-item';
|
|
1085
|
+
|
|
1086
|
+
let inputHtml = '';
|
|
1087
|
+
const val = config[key] !== undefined ? config[key] : (item.default || '');
|
|
1088
|
+
|
|
1089
|
+
if (item.type === 'boolean') {
|
|
1090
|
+
inputHtml = `<select id="conf-${key}">
|
|
1091
|
+
<option value="true" ${val === true ? 'selected' : ''}>True</option>
|
|
1092
|
+
<option value="false" ${val === false ? 'selected' : ''}>False</option>
|
|
1093
|
+
</select>`;
|
|
1094
|
+
} else if (item.type === 'integer' || item.type === 'number') {
|
|
1095
|
+
inputHtml = `<input type="number" id="conf-${key}" value="${val}">`;
|
|
1096
|
+
} else {
|
|
1097
|
+
inputHtml = `<input type="text" id="conf-${key}" value="${val}">`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
div.innerHTML = `
|
|
1101
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
1102
|
+
<label style="margin-bottom: 0;">${item.title || key}</label>
|
|
1103
|
+
<code style="font-size: 0.8em; color: #999; background: #f0f0f0; padding: 2px 4px; border-radius: 3px;">${key}</code>
|
|
1104
|
+
</div>
|
|
1105
|
+
<div style="margin-top: 8px;">${inputHtml}</div>
|
|
1106
|
+
${item.description ? `<div style="font-size: 0.85em; color: #666; margin-top: 6px; padding: 8px; background: #f9f9f9; border-left: 3px solid #ddd; font-style: italic;">💡 注释: ${item.description}</div>` : ''}
|
|
1107
|
+
`;
|
|
1108
|
+
body.appendChild(div);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function closeConfig() {
|
|
1113
|
+
document.getElementById('config-modal').style.display = 'none';
|
|
1114
|
+
currentEditingPlugin = null;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
document.getElementById('save-config-btn').onclick = async () => {
|
|
1118
|
+
const body = document.getElementById('modal-body');
|
|
1119
|
+
const inputs = body.querySelectorAll('input, select, textarea');
|
|
1120
|
+
const newConfig = {};
|
|
1121
|
+
inputs.forEach(input => {
|
|
1122
|
+
const key = input.id.replace('conf-', '');
|
|
1123
|
+
let val = input.value;
|
|
1124
|
+
if (input.tagName === 'SELECT') {
|
|
1125
|
+
val = val === 'true';
|
|
1126
|
+
} else if (input.type === 'number') {
|
|
1127
|
+
val = Number(val);
|
|
1128
|
+
}
|
|
1129
|
+
newConfig[key] = val;
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
try {
|
|
1133
|
+
const res = await authorizedFetch(`/web_console/api/plugins/${currentEditingPlugin}/config`, {
|
|
1134
|
+
method: 'POST',
|
|
1135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1136
|
+
body: JSON.stringify(newConfig)
|
|
1137
|
+
});
|
|
1138
|
+
const data = await res.json();
|
|
1139
|
+
if (data.success) {
|
|
1140
|
+
alert('配置已保存(部分配置可能需要重启生效)');
|
|
1141
|
+
closeConfig();
|
|
1142
|
+
} else {
|
|
1143
|
+
alert('保存失败: ' + (data.error || '未知错误'));
|
|
1144
|
+
}
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
alert('请求失败');
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
function toggleSidebar(show) {
|
|
1151
|
+
if (show) {
|
|
1152
|
+
sidebar.classList.remove('hidden');
|
|
1153
|
+
} else {
|
|
1154
|
+
sidebar.classList.add('hidden');
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
let currentLoginTab = 'code';
|
|
1159
|
+
|
|
1160
|
+
function switchLoginTab(tab) {
|
|
1161
|
+
currentLoginTab = tab;
|
|
1162
|
+
document.querySelectorAll('.login-tab').forEach(el => {
|
|
1163
|
+
el.classList.toggle('active', el.textContent.includes(tab === 'code' ? '验证码' : '密码'));
|
|
1164
|
+
});
|
|
1165
|
+
document.getElementById('login-code-section').style.display = tab === 'code' ? 'flex' : 'none';
|
|
1166
|
+
document.getElementById('login-password-section').style.display = tab === 'password' ? 'flex' : 'none';
|
|
1167
|
+
loginMsg.textContent = '';
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// 封装 fetch 以自动带上 Token
|
|
1171
|
+
async function authorizedFetch(url, options = {}) {
|
|
1172
|
+
options.headers = options.headers || {};
|
|
1173
|
+
if (authToken) {
|
|
1174
|
+
options.headers['Authorization'] = authToken;
|
|
1175
|
+
}
|
|
1176
|
+
const res = await fetch(url, options);
|
|
1177
|
+
if (res.status === 401) {
|
|
1178
|
+
showLogin();
|
|
1179
|
+
throw new Error('Unauthorized');
|
|
1180
|
+
}
|
|
1181
|
+
return res;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function showLogin() {
|
|
1185
|
+
authToken = null;
|
|
1186
|
+
localStorage.removeItem('web_console_token');
|
|
1187
|
+
appEl.classList.remove('authenticated');
|
|
1188
|
+
loginMask.classList.remove('hidden');
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function hideLogin() {
|
|
1192
|
+
appEl.classList.add('authenticated');
|
|
1193
|
+
loginMask.classList.add('hidden');
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// 发送验证码
|
|
1197
|
+
sendCodeBtn.onclick = async () => {
|
|
1198
|
+
sendCodeBtn.disabled = true;
|
|
1199
|
+
loginMsg.style.color = '#0078d4';
|
|
1200
|
+
loginMsg.textContent = '正在发送...';
|
|
1201
|
+
|
|
1202
|
+
try {
|
|
1203
|
+
const res = await fetch('/web_console/api/send_code', { method: 'POST' });
|
|
1204
|
+
const data = await res.json();
|
|
1205
|
+
if (data.error) {
|
|
1206
|
+
loginMsg.style.color = '#dc3545';
|
|
1207
|
+
loginMsg.textContent = data.error;
|
|
1208
|
+
sendCodeBtn.disabled = false;
|
|
1209
|
+
} else {
|
|
1210
|
+
loginMsg.style.color = '#28a745';
|
|
1211
|
+
loginMsg.textContent = data.msg;
|
|
1212
|
+
let count = 60;
|
|
1213
|
+
const timer = setInterval(() => {
|
|
1214
|
+
sendCodeBtn.textContent = `${count}s`;
|
|
1215
|
+
if (count <= 0) {
|
|
1216
|
+
clearInterval(timer);
|
|
1217
|
+
sendCodeBtn.disabled = false;
|
|
1218
|
+
sendCodeBtn.textContent = '获取验证码';
|
|
1219
|
+
}
|
|
1220
|
+
count--;
|
|
1221
|
+
}, 1000);
|
|
1222
|
+
}
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
loginMsg.style.color = '#dc3545';
|
|
1225
|
+
loginMsg.textContent = '发送失败,请检查网络';
|
|
1226
|
+
sendCodeBtn.disabled = false;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// 登录
|
|
1231
|
+
loginBtn.onclick = async () => {
|
|
1232
|
+
const code = document.getElementById('code-input').value.trim();
|
|
1233
|
+
const password = document.getElementById('password-input').value.trim();
|
|
1234
|
+
|
|
1235
|
+
const payload = {};
|
|
1236
|
+
if (currentLoginTab === 'code') {
|
|
1237
|
+
if (code.length !== 6) {
|
|
1238
|
+
loginMsg.style.color = '#dc3545';
|
|
1239
|
+
loginMsg.textContent = '请输入 6 位验证码';
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
payload.code = code;
|
|
1243
|
+
} else {
|
|
1244
|
+
if (!password) {
|
|
1245
|
+
loginMsg.style.color = '#dc3545';
|
|
1246
|
+
loginMsg.textContent = '请输入密码';
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
payload.password = password;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
try {
|
|
1253
|
+
const res = await fetch('/web_console/api/login', {
|
|
1254
|
+
method: 'POST',
|
|
1255
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1256
|
+
body: JSON.stringify(payload)
|
|
1257
|
+
});
|
|
1258
|
+
const data = await res.json();
|
|
1259
|
+
if (data.token) {
|
|
1260
|
+
authToken = data.token;
|
|
1261
|
+
localStorage.setItem('web_console_token', authToken);
|
|
1262
|
+
hideLogin();
|
|
1263
|
+
initApp();
|
|
1264
|
+
} else {
|
|
1265
|
+
loginMsg.style.color = '#dc3545';
|
|
1266
|
+
loginMsg.textContent = data.error || '登录失败';
|
|
1267
|
+
}
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
loginMsg.style.color = '#dc3545';
|
|
1270
|
+
loginMsg.textContent = '请求失败';
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
async function initApp() {
|
|
1275
|
+
initWS();
|
|
1276
|
+
await fetchChats();
|
|
1277
|
+
// 非移动端默认显示侧边栏
|
|
1278
|
+
if (window.innerWidth > 768) {
|
|
1279
|
+
toggleSidebar(true);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
showPage('home');
|
|
1283
|
+
// 定时刷新状态
|
|
1284
|
+
setInterval(() => {
|
|
1285
|
+
if (document.getElementById('page-home').classList.contains('active')) fetchStatus();
|
|
1286
|
+
if (document.getElementById('page-logs').classList.contains('active')) fetchLogs();
|
|
1287
|
+
}, 5000);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// 初始化
|
|
1291
|
+
window.onload = () => {
|
|
1292
|
+
initTheme();
|
|
1293
|
+
if (authToken) {
|
|
1294
|
+
hideLogin();
|
|
1295
|
+
initApp();
|
|
1296
|
+
} else {
|
|
1297
|
+
showLogin();
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
const chatListEl = document.getElementById('chat-list');
|
|
1302
|
+
const messagesEl = document.getElementById('messages');
|
|
1303
|
+
const msgInput = document.getElementById('msg-input');
|
|
1304
|
+
const sendBtn = document.getElementById('send-btn');
|
|
1305
|
+
const headerName = document.getElementById('current-chat-name');
|
|
1306
|
+
const statusEl = document.getElementById('status');
|
|
1307
|
+
|
|
1308
|
+
// 初始化 WebSocket
|
|
1309
|
+
function initWS() {
|
|
1310
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1311
|
+
ws = new WebSocket(`${protocol}//${window.location.host}/web_console/ws`);
|
|
1312
|
+
|
|
1313
|
+
ws.onopen = () => {
|
|
1314
|
+
statusEl.textContent = '● 已连接';
|
|
1315
|
+
statusEl.style.color = '#28a745';
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
ws.onclose = () => {
|
|
1319
|
+
statusEl.textContent = '○ 已断开';
|
|
1320
|
+
statusEl.style.color = '#dc3545';
|
|
1321
|
+
setTimeout(initWS, 3000); // 尝试重连
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
ws.onmessage = (event) => {
|
|
1325
|
+
const data = JSON.parse(event.data);
|
|
1326
|
+
if (data.type === 'new_message') {
|
|
1327
|
+
const chatItem = document.querySelector(`.chat-item[data-id="${data.chat_id}"]`);
|
|
1328
|
+
if (chatItem) {
|
|
1329
|
+
// 将活跃会话置顶
|
|
1330
|
+
chatListEl.prepend(chatItem);
|
|
1331
|
+
if (data.chat_id === currentChatId) {
|
|
1332
|
+
appendMessage(data.data);
|
|
1333
|
+
} else {
|
|
1334
|
+
chatItem.classList.add('has-new');
|
|
1335
|
+
}
|
|
1336
|
+
} else {
|
|
1337
|
+
// 发现新会话,刷新列表
|
|
1338
|
+
fetchChats();
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// 获取聊天列表
|
|
1345
|
+
async function fetchChats() {
|
|
1346
|
+
try {
|
|
1347
|
+
const res = await authorizedFetch('/web_console/api/chats');
|
|
1348
|
+
const data = await res.json();
|
|
1349
|
+
if (data.error) {
|
|
1350
|
+
chatListEl.innerHTML = `<div style="padding: 20px; color: red;">错误: ${data.error}</div>`;
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
renderChatList(data);
|
|
1355
|
+
} catch (e) {}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function renderChatList(data) {
|
|
1359
|
+
chatListEl.innerHTML = '';
|
|
1360
|
+
const allChats = [...data.groups, ...data.private];
|
|
1361
|
+
|
|
1362
|
+
allChats.forEach(chat => {
|
|
1363
|
+
const div = document.createElement('div');
|
|
1364
|
+
div.className = 'chat-item';
|
|
1365
|
+
div.dataset.id = chat.id;
|
|
1366
|
+
const isGroup = chat.id.startsWith('group_');
|
|
1367
|
+
|
|
1368
|
+
div.innerHTML = `
|
|
1369
|
+
<img class="avatar" src="${chat.avatar}" onerror="this.src='https://via.placeholder.com/40'">
|
|
1370
|
+
<div class="chat-info">
|
|
1371
|
+
<div class="chat-name">${chat.name} <span class="tag ${isGroup ? 'tag-group' : 'tag-private'}">${isGroup ? '群' : '私'}</span></div>
|
|
1372
|
+
<div class="unread-dot"></div>
|
|
1373
|
+
</div>
|
|
1374
|
+
`;
|
|
1375
|
+
|
|
1376
|
+
div.onclick = () => selectChat(chat, div);
|
|
1377
|
+
chatListEl.appendChild(div);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// 选择聊天
|
|
1382
|
+
async function selectChat(chat, element) {
|
|
1383
|
+
currentChatId = chat.id;
|
|
1384
|
+
headerName.textContent = chat.name;
|
|
1385
|
+
|
|
1386
|
+
// 移动端:选择聊天后隐藏侧边栏
|
|
1387
|
+
if (window.innerWidth <= 768) {
|
|
1388
|
+
toggleSidebar(false);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// 移除红点
|
|
1392
|
+
if (element) {
|
|
1393
|
+
element.classList.remove('has-new');
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// 更新 UI 激活状态
|
|
1397
|
+
document.querySelectorAll('.chat-item').forEach(el => {
|
|
1398
|
+
el.classList.toggle('active', el.dataset.id === chat.id);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
// 启用输入
|
|
1402
|
+
msgInput.disabled = false;
|
|
1403
|
+
sendBtn.disabled = false;
|
|
1404
|
+
msgInput.focus();
|
|
1405
|
+
|
|
1406
|
+
// 获取历史记录
|
|
1407
|
+
messagesEl.innerHTML = '<div style="margin: auto; color: #999;">加载中...</div>';
|
|
1408
|
+
try {
|
|
1409
|
+
const res = await authorizedFetch(`/web_console/api/history/${chat.id}`);
|
|
1410
|
+
const history = await res.json();
|
|
1411
|
+
|
|
1412
|
+
messagesEl.innerHTML = '';
|
|
1413
|
+
history.forEach(appendMessage);
|
|
1414
|
+
scrollToBottom();
|
|
1415
|
+
} catch (e) {}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function appendMessage(msg) {
|
|
1419
|
+
// 消息去重:如果该 ID 的消息已存在,则不再添加
|
|
1420
|
+
if (msg.id && document.getElementById(`msg-${msg.id}`)) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const wrapper = document.createElement('div');
|
|
1425
|
+
if (msg.id) wrapper.id = `msg-${msg.id}`; // 设置消息 ID 方便去重
|
|
1426
|
+
wrapper.className = `message-wrapper ${msg.is_self ? 'sent' : ''}`;
|
|
1427
|
+
|
|
1428
|
+
const avatarImg = `<img class="msg-avatar" src="${msg.sender_avatar}" onerror="this.src='https://via.placeholder.com/36'">`;
|
|
1429
|
+
|
|
1430
|
+
let contentHtml = '';
|
|
1431
|
+
if (msg.elements && msg.elements.length > 0) {
|
|
1432
|
+
msg.elements.forEach(el => {
|
|
1433
|
+
if (el.type === 'text') {
|
|
1434
|
+
contentHtml += `<span>${escapeHtml(el.data)}</span>`;
|
|
1435
|
+
} else if (el.type === 'image') {
|
|
1436
|
+
// 图片也走授权代理,动态注入 token
|
|
1437
|
+
let proxyUrl = el.data;
|
|
1438
|
+
if (proxyUrl.startsWith('/web_console/proxy/image')) {
|
|
1439
|
+
const separator = proxyUrl.includes('?') ? '&' : '?';
|
|
1440
|
+
proxyUrl += `${separator}token=${authToken}`;
|
|
1441
|
+
}
|
|
1442
|
+
contentHtml += `<img src="${proxyUrl}" onclick="window.open('${proxyUrl}')" onerror="this.src='https://via.placeholder.com/150?text=图片加载失败'" title="点击查看大图">`;
|
|
1443
|
+
} else if (el.type === 'face') {
|
|
1444
|
+
contentHtml += `<img class="face" src="${el.data}" onerror="this.src='https://via.placeholder.com/24?text=表情'" title="表情 ID: ${el.id}">`;
|
|
1445
|
+
} else if (el.type === 'at') {
|
|
1446
|
+
contentHtml += `<span class="at">@${el.data}</span>`;
|
|
1447
|
+
} else if (el.type === 'reply') {
|
|
1448
|
+
contentHtml += `<div class="reply-tag">回复消息 ID: ${el.data}</div>`;
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
} else {
|
|
1452
|
+
contentHtml = `<span>${escapeHtml(msg.content)}</span>`;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
wrapper.innerHTML = `
|
|
1456
|
+
${avatarImg}
|
|
1457
|
+
<div class="message ${msg.is_self ? 'sent' : 'received'}">
|
|
1458
|
+
<div class="msg-meta">${msg.sender_name}</div>
|
|
1459
|
+
<div class="msg-content">${contentHtml}</div>
|
|
1460
|
+
</div>
|
|
1461
|
+
`;
|
|
1462
|
+
|
|
1463
|
+
messagesEl.appendChild(wrapper);
|
|
1464
|
+
scrollToBottom();
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function escapeHtml(text) {
|
|
1468
|
+
const div = document.createElement('div');
|
|
1469
|
+
div.textContent = text;
|
|
1470
|
+
return div.innerHTML;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function scrollToBottom() {
|
|
1474
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// 发送消息
|
|
1478
|
+
async function sendMessage() {
|
|
1479
|
+
const content = msgInput.value.trim();
|
|
1480
|
+
if (!content || !currentChatId) return;
|
|
1481
|
+
|
|
1482
|
+
msgInput.value = '';
|
|
1483
|
+
try {
|
|
1484
|
+
const res = await authorizedFetch('/web_console/api/send', {
|
|
1485
|
+
method: 'POST',
|
|
1486
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1487
|
+
body: JSON.stringify({ chat_id: currentChatId, content })
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
const result = await res.json();
|
|
1491
|
+
if (result.error) {
|
|
1492
|
+
alert('发送失败: ' + result.error);
|
|
1493
|
+
}
|
|
1494
|
+
} catch (e) {}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
sendBtn.onclick = sendMessage;
|
|
1498
|
+
msgInput.onkeypress = (e) => { if (e.key === 'Enter') sendMessage(); };
|
|
1499
|
+
|
|
1500
|
+
// 初始化
|
|
1501
|
+
fetchChats();
|
|
1502
|
+
initWS();
|
|
1503
|
+
</script>
|
|
1504
|
+
</body>
|
|
1505
|
+
</html>
|