Z User commited on
Commit
d3d8112
·
1 Parent(s): 8f68434

redesign: 全中文暗黑系控制台,移除Key展示

Browse files

- 纯中文界面,无英文残留
- 移除所有API Key展示区域
- 左侧栏:系统状态/模型切换/配置概览/近期会话
- 主区域:全屏实时日志流,级别过滤,自动滚动
- 顶栏:在线状态灯/运行时间/内存/进程/操作按钮
- 深色极简风格,渐变装饰线

Files changed (1) hide show
  1. dashboard.html +311 -427
dashboard.html CHANGED
@@ -3,478 +3,362 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Hermes Bot — Dashboard</title>
 
 
7
  <style>
8
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
  :root{
10
- --bg:#0a0e17;--bg2:#111827;--bg3:#1a2332;--bg4:#232d3f;
11
- --border:#1e293b;--border2:#334155;
12
- --green:#00ff88;--blue:#00b4d8;--purple:#a78bfa;--red:#ff4757;--yellow:#fbbf24;--cyan:#22d3ee;--orange:#fb923c;
13
- --text:#e2e8f0;--text2:#94a3b8;--text3:#64748b;
14
- --mono:'JetBrains Mono','Fira Code','SF Mono',Monaco,Consolas,monospace;
15
- --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Noto Sans SC',sans-serif;
16
- --radius:12px;--radius-sm:8px;--radius-xs:6px;
17
  }
18
  html{font-size:14px}
19
- body{font-family:var(--sans);background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
20
- ::-webkit-scrollbar{width:6px;height:6px}
21
- ::-webkit-scrollbar-track{background:var(--bg2)}
22
  ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
23
- ::-webkit-scrollbar-thumb:hover{background:var(--text3)}
24
-
25
- /* ── Grid Layout ── */
26
- .dashboard{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto 1fr;gap:16px;padding:16px;max-width:1400px;margin:0 auto;min-height:100vh}
27
- @media(max-width:900px){.dashboard{grid-template-columns:1fr}}
28
-
29
- /* ── Header ── */
30
- .header{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;padding:16px 24px;background:linear-gradient(135deg,var(--bg2),var(--bg3));border:1px solid var(--border);border-radius:var(--radius);position:relative;overflow:hidden}
31
- .header::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--green),var(--blue),var(--purple))}
32
- .header-left{display:flex;align-items:center;gap:16px}
33
- .logo{font-size:1.8rem;font-weight:800;background:linear-gradient(135deg,var(--green),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.5px}
34
- .logo-sub{font-size:.75rem;color:var(--text3);margin-top:2px}
35
- .status-badge{display:flex;align-items:center;gap:8px;padding:6px 14px;border-radius:20px;font-size:.8rem;font-weight:600}
36
- .status-badge.online{background:rgba(0,255,136,.1);color:var(--green);border:1px solid rgba(0,255,136,.2)}
37
- .status-badge.offline{background:rgba(255,71,87,.1);color:var(--red);border:1px solid rgba(255,71,87,.2)}
38
- .pulse{width:8px;height:8px;border-radius:50%;background:currentColor;animation:pulse 2s infinite}
39
- @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.5)}}
40
- .header-right{display:flex;align-items:center;gap:12px}
41
- .header-btn{padding:8px 16px;border-radius:var(--radius-xs);border:1px solid var(--border2);background:var(--bg3);color:var(--text2);font-size:.8rem;cursor:pointer;transition:all .2s;font-family:var(--sans);display:flex;align-items:center;gap:6px}
42
- .header-btn:hover{border-color:var(--green);color:var(--green);background:rgba(0,255,136,.05)}
43
- .header-btn.danger:hover{border-color:var(--red);color:var(--red);background:rgba(255,71,87,.05)}
44
- .uptime{font-size:.75rem;color:var(--text3);font-family:var(--mono)}
45
-
46
- /* ── Panel ── */
47
- .panel{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column}
48
- .panel-head{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
49
- .panel-title{font-size:.85rem;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px}
50
- .panel-title .icon{font-size:1rem}
51
- .panel-body{padding:16px;flex:1;overflow-y:auto}
52
- .panel-body.no-pad{padding:0}
53
-
54
- /* ── Status Cards ── */
55
- .status-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
56
- .stat-card{padding:14px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);transition:all .2s}
57
- .stat-card:hover{border-color:var(--border2);transform:translateY(-1px)}
58
- .stat-label{font-size:.7rem;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
59
- .stat-value{font-size:1.1rem;font-weight:700;font-family:var(--mono);display:flex;align-items:center;gap:6px}
60
- .stat-value.green{color:var(--green)}
61
- .stat-value.blue{color:var(--blue)}
62
- .stat-value.purple{color:var(--purple)}
63
- .stat-value.yellow{color:var(--yellow)}
64
- .stat-value.cyan{color:var(--cyan)}
65
- .stat-sub{font-size:.65rem;color:var(--text3);margin-top:4px}
66
-
67
- /* ── Log Viewer ── */
68
- .log-container{font-family:var(--mono);font-size:.78rem;line-height:1.7;background:var(--bg);border-radius:var(--radius-sm);overflow:hidden;height:100%}
69
- .log-header{padding:8px 12px;background:var(--bg3);display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
70
- .log-controls{display:flex;gap:8px}
71
- .log-btn{padding:4px 10px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--text3);font-size:.7rem;cursor:pointer;font-family:var(--mono);transition:all .15s}
72
- .log-btn:hover,.log-btn.active{color:var(--green);border-color:var(--green)}
73
- .log-scroll{height:calc(100% - 40px);overflow-y:auto;padding:8px 12px}
74
- .log-line{display:flex;gap:10px;padding:1px 0;white-space:pre-wrap;word-break:break-all}
75
- .log-time{color:var(--text3);flex-shrink:0;user-select:none}
76
- .log-level{font-weight:600;flex-shrink:0;width:52px;text-align:right;user-select:none}
77
- .log-level.INFO{color:var(--green)}
78
- .log-level.WARN,.log-level.WARNING{color:var(--yellow)}
79
- .log-level.ERROR{color:var(--red)}
80
- .log-level.DEBUG{color:var(--text3)}
81
- .log-msg{color:var(--text)}
82
- .log-msg.hl{color:var(--cyan)}
83
- .log-msg.err{color:var(--red)}
84
- .log-empty{color:var(--text3);text-align:center;padding:40px;font-style:italic}
85
-
86
- /* ── Config Display ── */
87
- .config-section{margin-bottom:16px}
88
- .config-section:last-child{margin-bottom:0}
89
- .config-section-title{font-size:.75rem;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
90
- .config-row{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-xs);margin-bottom:6px;font-size:.8rem}
91
- .config-key{color:var(--text2);font-weight:500}
92
- .config-val{color:var(--text);font-family:var(--mono);font-size:.78rem;max-width:60%;text-align:right;overflow:hidden;text-overflow:ellipsis}
93
- .config-val.masked{color:var(--text3);letter-spacing:1px}
94
- .config-val.green{color:var(--green)}
95
- .config-val.red{color:var(--red)}
96
- .config-val.yellow{color:var(--yellow)}
97
-
98
- /* ── Session List ── */
99
- .session-item{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-xs);margin-bottom:6px;font-size:.8rem}
100
- .session-dot{width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0}
101
- .session-dot.idle{background:var(--text3)}
102
- .session-info{flex:1;min-width:0}
103
- .session-name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
104
- .session-meta{font-size:.7rem;color:var(--text3);margin-top:2px}
105
- .session-actions{display:flex;gap:6px}
106
-
107
- /* ── Model Selector ── */
108
- .model-select{width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);font-size:.8rem;font-family:var(--mono);cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}
109
- .model-select:focus{outline:none;border-color:var(--blue)}
110
- .model-select option{background:var(--bg2);color:var(--text)}
111
-
112
- /* ── Buttons ── */
113
- .btn{padding:8px 16px;border-radius:var(--radius-xs);border:1px solid var(--border2);font-size:.8rem;font-weight:500;cursor:pointer;transition:all .2s;font-family:var(--sans);display:inline-flex;align-items:center;gap:6px}
114
- .btn-green{background:rgba(0,255,136,.1);color:var(--green);border-color:rgba(0,255,136,.3)}
115
- .btn-green:hover{background:rgba(0,255,136,.2);box-shadow:0 0 20px rgba(0,255,136,.1)}
116
- .btn-blue{background:rgba(0,180,216,.1);color:var(--blue);border-color:rgba(0,180,216,.3)}
117
- .btn-blue:hover{background:rgba(0,180,216,.2)}
118
- .btn-red{background:rgba(255,71,87,.1);color:var(--red);border-color:rgba(255,71,87,.3)}
119
- .btn-red:hover{background:rgba(255,71,87,.2)}
120
- .btn-sm{padding:5px 10px;font-size:.72rem}
121
- .btn-group{display:flex;gap:8px;flex-wrap:wrap}
122
-
123
- /* ── Empty state ── */
124
- .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--text3);text-align:center}
125
- .empty-state .icon{font-size:2rem;margin-bottom:12px;opacity:.5}
126
-
127
- /* ── Tooltip ── */
128
- .tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.65rem;font-weight:600;letter-spacing:.3px}
129
- .tag-green{background:rgba(0,255,136,.1);color:var(--green)}
130
- .tag-blue{background:rgba(0,180,216,.1);color:var(--blue)}
131
- .tag-yellow{background:rgba(251,191,36,.1);color:var(--yellow)}
132
- .tag-red{background:rgba(255,71,87,.1);color:var(--red)}
133
- .tag-purple{background:rgba(167,139,250,.1);color:var(--purple)}
134
-
135
- /* ── Progress ── */
136
- .progress-bar{height:4px;background:var(--bg);border-radius:2px;overflow:hidden;margin-top:6px}
137
- .progress-fill{height:100%;border-radius:2px;transition:width .5s ease}
138
-
139
- /* ── Glow effect ── */
140
- .glow-green{box-shadow:0 0 15px rgba(0,255,136,.1)}
141
- .glow-blue{box-shadow:0 0 15px rgba(0,180,216,.1)}
142
-
143
- /* ── Animations ── */
144
- @keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
145
- .fade-in{animation:fadeIn .3s ease}
146
  @keyframes spin{to{transform:rotate(360deg)}}
147
- .spin{animation:spin 1s linear infinite}
148
-
149
- /* ── Toast ── */
150
- .toast-container{position:fixed;top:20px;right:20px;z-index:1000;display:flex;flex-direction:column;gap:8px}
151
- .toast{padding:12px 20px;border-radius:var(--radius-sm);font-size:.82rem;font-weight:500;animation:fadeIn .3s ease;display:flex;align-items:center;gap:8px;backdrop-filter:blur(10px)}
152
- .toast.success{background:rgba(0,255,136,.15);border:1px solid rgba(0,255,136,.3);color:var(--green)}
153
- .toast.error{background:rgba(255,71,87,.15);border:1px solid rgba(255,71,87,.3);color:var(--red)}
154
- .toast.info{background:rgba(0,180,216,.15);border:1px solid rgba(0,180,216,.3);color:var(--blue)}
 
 
 
 
 
 
 
 
 
 
 
 
155
  </style>
156
  </head>
157
  <body>
158
- <div id="toast-container" class="toast-container"></div>
159
- <div class="dashboard">
160
-
161
- <!-- ═══ Header ═══ -->
162
- <div class="header">
163
- <div class="header-left">
164
- <div>
165
- <div class="logo">⚕ Hermes Bot</div>
166
- <div class="logo-sub">HuggingFace Space · Real-time Dashboard / 实时监控面板</div>
167
- </div>
168
- </div>
169
- <div style="display:flex;align-items:center;gap:16px">
170
- <div id="status-badge" class="status-badge online"><span class="pulse"></span><span id="status-text">在线 Online</span></div>
171
- <div class="uptime" id="uptime">⏱ Uptime: --</div>
172
- </div>
173
- <div class="header-right">
174
- <button class="header-btn" onclick="restartGateway()">🔄 重启 Restart</button>
175
- <button class="header-btn" onclick="clearLogs()">🗑 清除日志 Clear</button>
176
- <button class="header-btn danger" onclick="factoryReset()">⚠ 重置 Reset</button>
177
  </div>
 
 
 
 
 
 
 
 
178
  </div>
179
 
180
- <!-- ═══ Left Column ═══ -->
181
- <div style="display:flex;flex-direction:column;gap:16px">
182
-
183
- <!-- Status Cards -->
184
- <div class="panel">
185
- <div class="panel-head"><div class="panel-title"><span class="icon">📊</span>系统状态 System Status</div><span class="tag tag-green" id="update-time">--</span></div>
186
- <div class="panel-body">
187
- <div class="status-grid" id="status-grid">
188
- <div class="stat-card"><div class="stat-label">平台 Platform</div><div class="stat-value green" id="s-platform">飞书 Feishu</div><div class="stat-sub" id="s-platform-sub">WebSocket</div></div>
189
- <div class="stat-card"><div class="stat-label">模型 Model</div><div class="stat-value blue" id="s-model">--</div><div class="stat-sub" id="s-model-sub">OpenRouter</div></div>
190
- <div class="stat-card"><div class="stat-label">会话 Sessions</div><div class="stat-value purple" id="s-sessions">0</div><div class="stat-sub" id="s-sessions-sub">活跃 Active</div></div>
191
- <div class="stat-card"><div class="stat-label">消息 Messages</div><div class="stat-value cyan" id="s-messages">0</div><div class="stat-sub" id="s-messages-sub">总计 Total</div></div>
192
- <div class="stat-card"><div class="stat-label">内存 RAM</div><div class="stat-value yellow" id="s-ram">--</div><div class="stat-sub" id="s-ram-sub">使用 Usage</div></div>
193
- <div class="stat-card"><div class="stat-label">PID</div><div class="stat-value" style="color:var(--text)" id="s-pid">--</div><div class="stat-sub">Gateway 进程</div></div>
194
- </div>
195
- </div>
196
  </div>
197
 
198
- <!-- Config Panel -->
199
- <div class="panel" style="flex:1">
200
- <div class="panel-head"><div class="panel-title"><span class="icon">⚙</span>配置 Configuration</div></div>
201
- <div class="panel-body" style="overflow-y:auto">
202
- <div class="config-section">
203
- <div class="config-section-title">🔑 API Keys(已脱敏 Masked)</div>
204
- <div class="config-row"><span class="config-key">OPENROUTER_API_KEY</span><span class="config-val masked" id="c-openrouter">--</span></div>
205
- <div class="config-row"><span class="config-key">FEISHU_APP_ID</span><span class="config-val" id="c-feishu-id">--</span></div>
206
- <div class="config-row"><span class="config-key">FEISHU_APP_SECRET</span><span class="config-val masked" id="c-feishu-secret">--</span></div>
207
- </div>
208
- <div class="config-section">
209
- <div class="config-section-title">🤖 模型 Model</div>
210
- <select class="model-select" id="model-select" onchange="changeModel(this.value)">
211
- <option value="inclusionai/ling-2.6-1t:free">inclusionai/ling-2.6-1t:free</option>
212
- <option value="inclusionai/ling-2.6-flash:free">inclusionai/ling-2.6-flash:free</option>
213
- <option value="google/gemma-4-31b-it:free">google/gemma-4-31b-it:free</option>
214
- <option value="qwen/qwen3-coder:free">qwen/qwen3-coder:free</option>
215
- <option value="qwen/qwen3-next-80b-a3b-instruct:free">qwen/qwen3-next-80b-a3b-instruct:free</option>
216
- <option value="openai/gpt-oss-120b:free">openai/gpt-oss-120b:free</option>
217
- <option value="z-ai/glm-4.5-air:free">z-ai/glm-4.5-air:free</option>
218
- <option value="nvidia/nemotron-3-super-120b-a12b:free">nvidia/nemotron-3-super-120b-a12b:free</option>
219
- </select>
220
- <div style="margin-top:8px" class="btn-group">
221
- <button class="btn btn-green btn-sm" onclick="changeModel(document.getElementById('model-select').value)">✓ 切换模型 Switch</button>
222
- </div>
223
- </div>
224
- <div class="config-section">
225
- <div class="config-section-title">🧠 记忆 Memory</div>
226
- <div class="config-row"><span class="config-key">Provider</span><span class="config-val" id="c-memory">none</span></div>
227
- <div class="config-row"><span class="config-key">Sessions</span><span class="config-val" id="c-sessions-dir">/data/hermes/sessions</span></div>
228
- <div class="config-row"><span class="config-key">Memories</span><span class="config-val" id="c-memories-dir">/data/hermes/memories</span></div>
229
- </div>
230
- <div class="config-section">
231
- <div class="config-section-title">🔧 高级 Advanced</div>
232
- <div class="config-row"><span class="config-key">Terminal</span><span class="config-val" id="c-terminal">local</span></div>
233
- <div class="config-row"><span class="config-key">Timezone</span><span class="config-val" id="c-tz">Asia/Shanghai</span></div>
234
- <div class="config-row"><span class="config-key">Max Turns</span><span class="config-val" id="c-turns">90</span></div>
235
- <div class="config-row"><span class="config-key">MCP</span><span class="config-val" id="c-mcp">disabled</span></div>
236
- <div class="config-row"><span class="config-key">Compress</span><span class="config-val green" id="c-compress">enabled</span></div>
237
- </div>
238
  </div>
239
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  </div>
241
 
242
- <!-- ═══ Right Column ═══ -->
243
- <div style="display:flex;flex-direction:column;gap:16px">
244
-
245
- <!-- Log Viewer -->
246
- <div class="panel" style="flex:1;min-height:400px">
247
- <div class="panel-head">
248
- <div class="panel-title"><span class="icon">📜</span>实时日志 Live Logs</div>
249
- <div class="log-controls">
250
- <button class="log-btn active" id="btn-auto" onclick="toggleAutoScroll()">⏬ Auto</button>
251
- <button class="log-btn" id="btn-info" onclick="toggleLevel('INFO')">INFO</button>
252
- <button class="log-btn active" id="btn-warn" onclick="toggleLevel('WARN')">WARN</button>
253
- <button class="log-btn active" id="btn-error" onclick="toggleLevel('ERROR')">ERR</button>
254
- </div>
255
- </div>
256
- <div class="panel-body no-pad" style="flex:1;display:flex;flex-direction:column">
257
- <div class="log-container" style="flex:1;display:flex;flex-direction:column">
258
- <div class="log-scroll" id="log-scroll">
259
- <div class="log-empty">等待日志... Waiting for logs...</div>
260
- </div>
261
- </div>
262
  </div>
263
  </div>
264
-
265
- <!-- Sessions -->
266
- <div class="panel" style="max-height:250px">
267
- <div class="panel-head"><div class="panel-title"><span class="icon">💬</span>会话 Sessions</div><button class="btn btn-blue btn-sm" onclick="loadSessions()">↻ 刷新 Refresh</button></div>
268
- <div class="panel-body" style="overflow-y:auto;max-height:180px" id="sessions-list">
269
- <div class="empty-state"><div class="icon">💬</div><div>暂无会话 No sessions yet</div></div>
270
  </div>
271
  </div>
 
 
 
 
272
  </div>
273
 
274
  </div>
275
 
276
  <script>
277
- const API = '';
278
- let autoScroll = true;
279
- let visibleLevels = { INFO: true, WARN: true, ERROR: true, DEBUG: false, WARNING: true };
280
- let logBuffer = [];
281
- const MAX_LOGS = 500;
282
- let startTs = Date.now();
283
-
284
- // ─��� Toast ──
285
- function toast(msg, type='info') {
286
- const el = document.createElement('div');
287
- el.className = 'toast ' + type;
288
- el.textContent = msg;
289
- document.getElementById('toast-container').appendChild(el);
290
- setTimeout(() => el.remove(), 4000);
291
- }
292
-
293
- // ── Mask key ──
294
- function maskKey(key) {
295
- if (!key || key.length < 8) return '••••••••';
296
- return key.substring(0, 6) + '••••' + key.substring(key.length - 4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
298
 
299
- // ── Format uptime ──
300
- function fmtUptime(ms) {
301
- const s = Math.floor(ms / 1000);
302
- const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
303
- return d > 0 ? `${d}d ${h}h ${m}m` : h > 0 ? `${h}h ${m}m` : `${m}m ${s % 60}s`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
 
306
- // ── SSE Logs ──
307
- function connectLogs() {
308
- const es = new EventSource(API + '/api/logs/stream');
309
- es.onmessage = (e) => {
310
- try {
311
- const lines = JSON.parse(e.data);
312
- if (!Array.isArray(lines)) return;
313
- lines.forEach(l => appendLog(l));
314
- } catch(err) {}
315
- };
316
- es.onerror = () => setTimeout(connectLogs, 3000);
317
- }
318
-
319
- function appendLog(l) {
320
- const scroll = document.getElementById('log-scroll');
321
- if (logBuffer.length === 0) scroll.innerHTML = '';
322
- logBuffer.push(l);
323
- if (logBuffer.length > MAX_LOGS) logBuffer.shift();
324
-
325
- const time = l.time || '';
326
- const level = l.level || 'INFO';
327
- const msg = l.msg || '';
328
- const hl = l.hl || false;
329
- const err = level === 'ERROR';
330
-
331
- const div = document.createElement('div');
332
- div.className = 'log-line';
333
- div.dataset.level = level;
334
- if (!visibleLevels[level]) div.style.display = 'none';
335
- div.innerHTML = `<span class="log-time">${escHtml(time)}</span><span class="log-level ${level}">${level.padEnd(5)}</span><span class="log-msg${hl ? ' hl' : ''}${err ? ' err' : ''}">${escHtml(msg)}</span>`;
336
- scroll.appendChild(div);
337
-
338
- while (scroll.children.length > MAX_LOGS) scroll.removeChild(scroll.firstChild);
339
- if (autoScroll) scroll.scrollTop = scroll.scrollHeight;
340
- }
341
-
342
- function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
343
-
344
- function toggleAutoScroll() {
345
- autoScroll = !autoScroll;
346
- document.getElementById('btn-auto').classList.toggle('active', autoScroll);
347
- }
348
-
349
- function toggleLevel(level) {
350
- visibleLevels[level] = !visibleLevels[level];
351
- document.getElementById('btn-' + level.toLowerCase()).classList.toggle('active', visibleLevels[level]);
352
- document.querySelectorAll('#log-scroll .log-line').forEach(el => {
353
- el.style.display = visibleLevels[el.dataset.level] ? '' : 'none';
354
- });
355
- }
356
-
357
- // ── Status Polling ──
358
- async function pollStatus() {
359
- try {
360
- const r = await fetch(API + '/api/status');
361
- const d = await r.json();
362
- updateUI(d);
363
- } catch(e) {}
364
- }
365
-
366
- function updateUI(d) {
367
- // Status badge
368
- const badge = document.getElementById('status-badge');
369
- const stxt = document.getElementById('status-text');
370
- if (d.running) {
371
- badge.className = 'status-badge online';
372
- stxt.textContent = '在线 Online';
373
- } else {
374
- badge.className = 'status-badge offline';
375
- stxt.textContent = '离线 Offline';
376
- }
377
-
378
- document.getElementById('s-model').textContent = (d.model || '--').split('/').pop().substring(0, 28);
379
- document.getElementById('s-model-sub').textContent = d.provider || 'OpenRouter';
380
- document.getElementById('s-sessions').textContent = d.sessions || 0;
381
- document.getElementById('s-messages').textContent = d.messages || 0;
382
- document.getElementById('s-ram').textContent = d.ram || '--';
383
- document.getElementById('s-pid').textContent = d.pid || '--';
384
- document.getElementById('s-platform').textContent = d.platform || '飞书 Feishu';
385
-
386
- if (d.platform_mode) document.getElementById('s-platform-sub').textContent = d.platform_mode;
387
- if (d.uptime_ms) document.getElementById('uptime').textContent = '⏱ Uptime: ' + fmtUptime(d.uptime_ms);
388
-
389
- document.getElementById('update-time').textContent = new Date().toLocaleTimeString('zh-CN');
390
-
391
- // Config
392
- if (d.config) {
393
- document.getElementById('c-openrouter').textContent = maskKey(d.config.OPENROUTER_API_KEY);
394
- document.getElementById('c-feishu-id').textContent = d.config.FEISHU_APP_ID || '--';
395
- document.getElementById('c-feishu-secret').textContent = maskKey(d.config.FEISHU_APP_SECRET);
396
- document.getElementById('c-terminal').textContent = d.config.terminal || 'local';
397
- document.getElementById('c-tz').textContent = d.config.timezone || 'Asia/Shanghai';
398
- document.getElementById('c-turns').textContent = d.config.max_turns || '90';
399
- document.getElementById('c-memory').textContent = d.config.memory || 'none';
400
- if (d.config.no_mcp) document.getElementById('c-mcp').textContent = 'disabled';
401
- if (d.config.compress) document.getElementById('c-compress').textContent = 'enabled';
402
- if (d.model) {
403
- const sel = document.getElementById('model-select');
404
- sel.value = d.model;
405
- const opt = sel.querySelector(`option[value="${d.model}"]`);
406
- if (!opt) {
407
- const o = document.createElement('option');
408
- o.value = d.model; o.textContent = d.model;
409
- sel.prepend(o); sel.value = d.model;
410
  }
411
  }
412
- }
 
413
  }
414
 
415
- // ── Sessions ──
416
- async function loadSessions() {
417
- try {
418
- const r = await fetch(API + '/api/sessions');
419
- const list = await r.json();
420
- const el = document.getElementById('sessions-list');
421
- if (!list || list.length === 0) {
422
- el.innerHTML = '<div class="empty-state"><div class="icon">💬</div><div>暂无会话 No sessions yet</div></div>';
423
- return;
424
- }
425
- el.innerHTML = list.map(s => `
426
- <div class="session-item fade-in">
427
- <span class="session-dot ${s.active ? '' : 'idle'}"></span>
428
- <div class="session-info">
429
- <div class="session-name">${escHtml(s.name || s.id)}</div>
430
- <div class="session-meta">${escHtml(s.platform || '')} · ${s.messages || 0} msgs · ${escHtml(s.last || '')}</div>
431
  </div>
432
  </div>`).join('');
433
- } catch(e) {}
434
  }
435
 
436
- // ── Actions ──
437
- async function restartGateway() {
438
- toast('正在重启... Restarting...', 'info');
439
- try {
440
- const r = await fetch(API + '/api/restart', { method: 'POST' });
441
- const d = await r.json();
442
- toast(d.ok ? '✅ 重启成功!Restarted!' : '❌ ' + (d.error || 'Failed'), d.ok ? 'success' : 'error');
443
- } catch(e) { toast('❌ Request failed', 'error'); }
444
  }
445
-
446
- async function clearLogs() {
447
- logBuffer = [];
448
- document.getElementById('log-scroll').innerHTML = '<div class="log-empty">已清除 Cleared</div>';
449
- toast('🗑 日志已清除 Logs cleared', 'success');
450
  }
451
-
452
- async function changeModel(model) {
453
- toast('切换模型: ' + model, 'info');
454
- try {
455
- const r = await fetch(API + '/api/model', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ model }) });
456
- const d = await r.json();
457
- toast(d.ok ? '✅ 模型已切换 Model switched!' : '❌ ' + (d.error || 'Failed'), d.ok ? 'success' : 'error');
458
- if (d.ok) setTimeout(pollStatus, 1000);
459
- } catch(e) { toast('❌ Request failed', 'error'); }
 
 
 
 
 
460
  }
461
 
462
- async function factoryReset() {
463
- if (!confirm('⚠ 确认重置?This will reset configuration.\n所有配置将被恢复默认。All configs will be reset.')) return;
464
- toast('正在重置... Resetting...', 'info');
465
- try {
466
- const r = await fetch(API + '/api/reset', { method: 'POST' });
467
- const d = await r.json();
468
- toast(d.ok ? '✅ 已重置!Reset done!' : '❌ ' + (d.error || 'Failed'), d.ok ? 'success' : 'error');
469
- } catch(e) { toast('❌ Request failed', 'error'); }
470
  }
 
471
 
472
- // ── Init ──
473
- pollStatus();
474
- setInterval(pollStatus, 5000);
475
- setInterval(loadSessions, 30000);
476
- loadSessions();
477
- connectLogs();
478
  </script>
479
  </body>
480
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Hermes · 控制台</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
9
  <style>
10
+ *{margin:0;padding:0;box-sizing:border-box}
11
  :root{
12
+ --bg:#06090f;--bg2:#0c1018;--bg3:#121825;--bg4:#1a2233;
13
+ --border:#1c2536;--border2:#2a3a52;
14
+ --g:#10ffb0;--b:#3b9eff;--p:#a371f7;--r:#ff5068;--y:#ffc53d;--c:#2dd4bf;--o:#ff8a4c;
15
+ --t1:#f0f2f5;--t2:#8b97a8;--t3:#556073;
16
+ --f:-apple-system,BlinkMacSystemFont,"Inter","Noto Sans SC","PingFang SC",sans-serif;
17
+ --m:"JetBrains Mono","SF Mono",Monaco,Consolas,monospace;
 
18
  }
19
  html{font-size:14px}
20
+ body{font-family:var(--f);background:var(--bg);color:var(--t1);min-height:100vh;overflow:hidden}
21
+ ::-webkit-scrollbar{width:5px}
22
+ ::-webkit-scrollbar-track{background:transparent}
23
  ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
24
+
25
+ .layout{display:grid;grid-template-rows:56px 1fr;grid-template-columns:280px 1fr;height:100vh}
26
+
27
+ /* ─── 顶栏 ─── */
28
+ .topbar{grid-column:1/-1;display:flex;align-items:center;padding:0 20px;background:var(--bg2);border-bottom:1px solid var(--border);gap:16px;z-index:10}
29
+ .brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:1.05rem;color:var(--t1);letter-spacing:-.3px;flex-shrink:0}
30
+ .brand svg{width:22px;height:22px}
31
+ .brand-sub{font-size:.68rem;color:var(--t3);font-weight:400;margin-left:4px}
32
+ .dot-live{width:7px;height:7px;border-radius:50%;background:var(--g);box-shadow:0 0 8px var(--g);animation:pulse 2s ease infinite}
33
+ .dot-live.off{background:var(--r);box-shadow:0 0 8px var(--r)}
34
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
35
+ .topbar-spacer{flex:1}
36
+ .topbar-stat{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--t2)}
37
+ .topbar-stat b{color:var(--t1);font-family:var(--m);font-weight:600}
38
+ .topbar-btn{height:32px;padding:0 14px;border-radius:6px;border:1px solid var(--border2);background:transparent;color:var(--t2);font-size:.75rem;cursor:pointer;font-family:var(--f);font-weight:500;transition:all .15s;display:flex;align-items:center;gap:5px}
39
+ .topbar-btn:hover{border-color:var(--g);color:var(--g);background:rgba(16,255,176,.04)}
40
+ .topbar-btn.danger:hover{border-color:var(--r);color:var(--r);background:rgba(255,80,104,.04)}
41
+
42
+ /* ─── 侧栏 ─── */
43
+ .sidebar{background:var(--bg2);border-right:1px solid var(--border);overflow-y:auto;padding:16px 12px;display:flex;flex-direction:column;gap:20px}
44
+ .sb-section{display:flex;flex-direction:column;gap:8px}
45
+ .sb-title{font-size:.65rem;color:var(--t3);text-transform:uppercase;letter-spacing:1.2px;font-weight:600;padding:0 8px}
46
+ .sb-card{padding:12px 14px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;transition:border-color .15s}
47
+ .sb-card:hover{border-color:var(--border2)}
48
+ .sb-card .label{font-size:.68rem;color:var(--t3);margin-bottom:4px;font-weight:500}
49
+ .sb-card .value{font-size:.92rem;font-weight:700;font-family:var(--m);display:flex;align-items:center;gap:6px}
50
+ .sb-card .value.cg{color:var(--g)}.sb-card .value.cb{color:var(--b)}.sb-card .value.cp{color:var(--p)}.sb-card .value.cy{color:var(--y)}.sb-card .value.cc{color:var(--c)}.sb-card .value.co{color:var(--o)}
51
+ .sb-card .sub{font-size:.62rem;color:var(--t3);margin-top:3px;font-family:var(--m)}
52
+
53
+ /* 模型选择 */
54
+ .model-select{width:100%;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--t1);font-size:.75rem;font-family:var(--m);cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' fill='%23556073'%3E%3Cpath d='M5 7L0 2h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;transition:border-color .15s}
55
+ .model-select:focus{outline:none;border-color:var(--b)}
56
+ .model-select option{background:var(--bg2);color:var(--t1)}
57
+ .btn-row{display:flex;gap:6px;margin-top:4px}
58
+ .btn-s{flex:1;height:30px;border-radius:6px;border:1px solid var(--border2);background:transparent;color:var(--t2);font-size:.7rem;cursor:pointer;font-family:var(--f);font-weight:500;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:4px}
59
+ .btn-s:hover{border-color:var(--g);color:var(--g)}
60
+ .btn-s.btn-g{background:rgba(16,255,176,.08);border-color:rgba(16,255,176,.2);color:var(--g)}
61
+ .btn-s.btn-g:hover{background:rgba(16,255,176,.14)}
62
+ .btn-s.btn-b{background:rgba(59,158,255,.08);border-color:rgba(59,158,255,.2);color:var(--b)}
63
+ .btn-s.btn-b:hover{background:rgba(59,158,255,.14)}
64
+
65
+ /* 会话列表 */
66
+ .session-item{display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--bg);border-radius:6px;font-size:.75rem}
67
+ .s-dot{width:6px;height:6px;border-radius:50%;background:var(--g);flex-shrink:0}
68
+ .s-dot.idle{background:var(--t3)}
69
+ .s-info{flex:1;min-width:0}
70
+ .s-name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
71
+ .s-meta{font-size:.6rem;color:var(--t3);margin-top:1px;font-family:var(--m)}
72
+ .empty-hint{font-size:.75rem;color:var(--t3);text-align:center;padding:20px 0;font-style:italic}
73
+
74
+ /* ─── 主区域 ─── */
75
+ .main{display:flex;flex-direction:column;overflow:hidden;background:var(--bg)}
76
+ .log-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;border-bottom:1px solid var(--border);flex-shrink:0;min-height:38px}
77
+ .log-bar-title{font-size:.75rem;font-weight:600;color:var(--t2);display:flex;align-items:center;gap:6px}
78
+ .log-bar-right{display:flex;align-items:center;gap:4px}
79
+ .lf{height:24px;padding:0 8px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--t3);font-size:.65rem;cursor:pointer;font-family:var(--m);font-weight:500;transition:all .12s}
80
+ .lf:hover,.lf.on{color:var(--g);border-color:rgba(16,255,176,.3)}
81
+ .lf.on{background:rgba(16,255,176,.06)}
82
+ .lf-divider{width:1px;height:14px;background:var(--border);margin:0 2px}
83
+
84
+ /* 日志区域 */
85
+ .log-area{flex:1;overflow:hidden;position:relative}
86
+ .log-scroll{position:absolute;inset:0;overflow-y:auto;padding:10px 16px;font-family:var(--m);font-size:.74rem;line-height:1.75}
87
+ .log-line{display:flex;gap:12px;padding:0 2px;border-radius:3px;transition:background .1s}
88
+ .log-line:hover{background:rgba(255,255,255,.015)}
89
+ .lt{color:var(--t3);flex-shrink:0;user-select:none;font-size:.7rem;min-width:68px}
90
+ .ll{font-weight:600;flex-shrink:0;width:48px;text-align:right;user-select:none;font-size:.68rem}
91
+ .ll.I{color:var(--g)}.ll.W{color:var(--y)}.ll.E{color:var(--r)}.ll.D{color:var(--t3)}
92
+ .lm{color:var(--t2);word-break:break-all;flex:1}
93
+ .lm.hl{color:var(--c)}
94
+ .lm.err{color:var(--r)}
95
+ .log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:.8rem}
96
+ .log-empty::before{content:'';display:block;width:80px;height:80px;border-radius:50%;border:2px dashed var(--border);margin-bottom:12px;background:radial-gradient(circle,var(--bg3),transparent);animation:spin 20s linear infinite}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  @keyframes spin{to{transform:rotate(360deg)}}
98
+
99
+ /* 底栏 */
100
+ .log-footer{display:flex;align-items:center;justify-content:space-between;padding:6px 16px;border-top:1px solid var(--border);flex-shrink:0;font-size:.65rem;color:var(--t3);font-family:var(--m)}
101
+
102
+ /* 吐司 */
103
+ .toast-box{position:fixed;top:64px;right:16px;z-index:100;display:flex;flex-direction:column;gap:6px}
104
+ .toast{padding:10px 18px;border-radius:8px;font-size:.78rem;font-weight:500;animation:fadeUp .25s ease;backdrop-filter:blur(12px)}
105
+ .toast.ok{background:rgba(16,255,176,.12);border:1px solid rgba(16,255,176,.25);color:var(--g)}
106
+ .toast.err{background:rgba(255,80,104,.12);border:1px solid rgba(255,80,104,.25);color:var(--r)}
107
+ .toast.info{background:rgba(59,158,255,.12);border:1px solid rgba(59,158,255,.25);color:var(--b)}
108
+ @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
109
+
110
+ /* 亮条装饰 */
111
+ .topbar::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--g),var(--b),var(--p),transparent);opacity:.3}
112
+
113
+ /* 响应式 */
114
+ @media(max-width:768px){
115
+ .layout{grid-template-columns:1fr}
116
+ .sidebar{display:none}
117
+ }
118
  </style>
119
  </head>
120
  <body>
121
+ <div class="toast-box" id="tb"></div>
122
+ <div class="layout">
123
+
124
+ <!-- 顶栏 -->
125
+ <div class="topbar" style="position:relative">
126
+ <div class="brand">
127
+ <svg viewBox="0 0 24 24" fill="none" stroke="var(--g)" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
128
+ Hermes 控制台
129
+ <span class="brand-sub">v0.11</span>
 
 
 
 
 
 
 
 
 
 
130
  </div>
131
+ <div class="dot-live" id="dot"></div>
132
+ <div class="topbar-stat">运行 <b id="uptime">--</b></div>
133
+ <div class="topbar-stat">内存 <b id="ram">--</b></div>
134
+ <div class="topbar-stat">进程 <b id="pid">--</b></div>
135
+ <div class="topbar-spacer"></div>
136
+ <button class="topbar-btn" onclick="doRestart()">↻ 重启</button>
137
+ <button class="topbar-btn" onclick="doClear()">清除日志</button>
138
+ <button class="topbar-btn danger" onclick="doReset()">重置配置</button>
139
  </div>
140
 
141
+ <!-- 侧栏 -->
142
+ <div class="sidebar">
143
+
144
+ <!-- 系统状态 -->
145
+ <div class="sb-section">
146
+ <div class="sb-title">系统状态</div>
147
+ <div class="sb-card"><div class="label">平台</div><div class="value cg" id="x-plat">飞书</div><div class="sub" id="x-plat-sub">WebSocket 长连接</div></div>
148
+ <div class="sb-card"><div class="label">模型</div><div class="value cb" id="x-model">--</div><div class="sub" id="x-model-sub">OpenRouter</div></div>
149
+ <div class="sb-card"><div class="label">会话数</div><div class="value cp" id="x-sess">0</div><div class="sub">活跃会话</div></div>
150
+ <div class="sb-card"><div class="label">终端</div><div class="value cc" id="x-term">本地</div><div class="sub" id="x-tz">Asia/Shanghai</div></div>
 
 
 
 
 
 
151
  </div>
152
 
153
+ <!-- 模型切换 -->
154
+ <div class="sb-section">
155
+ <div class="sb-title">模型切换</div>
156
+ <select class="model-select" id="msel"></select>
157
+ <div class="btn-row">
158
+ <button class="btn-s btn-g" onclick="doSwitch()">切换</button>
159
+ <button class="btn-s btn-b" onclick="loadSess()">刷新会话</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  </div>
161
  </div>
162
+
163
+ <!-- 配置概览 -->
164
+ <div class="sb-section">
165
+ <div class="sb-title">配置概览</div>
166
+ <div class="sb-card"><div class="label">飞书应用</div><div class="value co" id="x-appid">--</div></div>
167
+ <div class="sb-card"><div class="label">连接模式</div><div class="value cb" id="x-conn">WebSocket</div></div>
168
+ <div class="sb-card"><div class="label">最大轮数</div><div class="value cy" id="x-turns">90</div></div>
169
+ <div class="sb-card"><div class="label">压缩</div><div class="value cg" id="x-comp">已开启</div></div>
170
+ <div class="sb-card"><div class="label">记忆</div><div class="value" id="x-mem" style="color:var(--t2)">未配置</div></div>
171
+ </div>
172
+
173
+ <!-- 近期会话 -->
174
+ <div class="sb-section" style="flex:1;min-height:0">
175
+ <div class="sb-title">近期会话</div>
176
+ <div id="sess-list"><div class="empty-hint">暂无会话</div></div>
177
+ </div>
178
+
179
  </div>
180
 
181
+ <!-- 主区域 -->
182
+ <div class="main">
183
+ <div class="log-bar">
184
+ <div class="log-bar-title">📋 实时日志</div>
185
+ <div class="log-bar-right">
186
+ <button class="lf on" id="fI" onclick="togF('I')">信息</button>
187
+ <button class="lf on" id="fW" onclick="togF('W')">警告</button>
188
+ <button class="lf on" id="fE" onclick="togF('E')">错误</button>
189
+ <div class="lf-divider"></div>
190
+ <button class="lf on" id="fAuto" onclick="togAuto()">自动滚动</button>
191
+ <button class="lf" onclick="togAuto();document.getElementById('log-scroll').scrollTop=99999">↓ 到底部</button>
 
 
 
 
 
 
 
 
 
192
  </div>
193
  </div>
194
+ <div class="log-area">
195
+ <div class="log-scroll" id="log-scroll">
196
+ <div class="log-empty">等待日志输出…</div>
 
 
 
197
  </div>
198
  </div>
199
+ <div class="log-footer">
200
+ <span id="log-count">0 条日志</span>
201
+ <span id="log-time"></span>
202
+ </div>
203
  </div>
204
 
205
  </div>
206
 
207
  <script>
208
+ const A='';
209
+ let autoScroll=true, totalLogs=0;
210
+ const filters={I:true,W:true,E:true};
211
+ const logBuf=[];
212
+ const MAX=800;
213
+
214
+ // 免费模型列表
215
+ const MODELS=[
216
+ {v:'inclusionai/ling-2.6-1t:free',l:'Ling 2.6 (免费)'},
217
+ {v:'inclusionai/ling-2.6-flash:free',l:'Ling 2.6 Flash (免费)'},
218
+ {v:'google/gemma-4-31b-it:free',l:'Gemma 4 31B (免费)'},
219
+ {v:'qwen/qwen3-coder:free',l:'Qwen3 Coder (免费)'},
220
+ {v:'qwen/qwen3-next-80b-a3b-instruct:free',l:'Qwen3 Next 80B (免费)'},
221
+ {v:'openai/gpt-oss-120b:free',l:'GPT OSS 120B (免费)'},
222
+ {v:'z-ai/glm-4.5-air:free',l:'GLM 4.5 Air (免费)'},
223
+ {v:'nvidia/nemotron-3-super-120b-a12b:free',l:'Nemotron 3 Super (免费)'},
224
+ ];
225
+
226
+ // 初始化模型下拉
227
+ (function(){
228
+ const sel=document.getElementById('msel');
229
+ MODELS.forEach(m=>{const o=document.createElement('option');o.value=m.v;o.textContent=m.l;sel.appendChild(o)});
230
+ })();
231
+
232
+ // 吐司
233
+ function toast(m,t='info'){const e=document.createElement('div');e.className='toast '+t;e.textContent=m;document.getElementById('tb').appendChild(e);setTimeout(()=>e.remove(),3500)}
234
+
235
+ // 格式化运行时间
236
+ function fmtUp(ms){const s=Math.floor(ms/1000),d=Math.floor(s/86400),h=Math.floor(s%86400/3600),mi=Math.floor(s%3600/60);return d>0?d+'天 '+h+'时 '+mi+'分':h>0?h+'时 '+mi+'分':mi+'分 '+(s%60)+'秒'}
237
+
238
+ // HTML转义
239
+ function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
240
+
241
+ // ── SSE 日志 ──
242
+ function connectSSE(){
243
+ const es=new EventSource(A+'/api/logs/stream');
244
+ es.onmessage=e=>{try{const a=JSON.parse(e.data);if(Array.isArray(a))a.forEach(addLog)}catch{}};
245
+ es.onerror=()=>setTimeout(connectSSE,3000);
246
  }
247
 
248
+ function addLog(l){
249
+ const sc=document.getElementById('log-scroll');
250
+ if(!totalLogs)sc.innerHTML='';
251
+ totalLogs++;logBuf.push(l);
252
+ if(logBuf.length>MAX)logBuf.shift();
253
+ const lv=l.level?.toUpperCase()||'I';
254
+ const map={INFO:'I',WARN:'W',WARNING:'W',ERROR:'E',DEBUG:'D'};
255
+ const fk=map[lv]||'I';
256
+ const div=document.createElement('div');
257
+ div.className='log-line';div.dataset.f=fk;
258
+ if(!filters[fk])div.style.display='none';
259
+ const hl=(l.msg||'').toLowerCase();
260
+ const isHL=hl.includes('feishu')||hl.includes('飞书')||hl.includes('connect')||hl.includes('收到')||hl.includes('回复');
261
+ const isErr=fk==='E';
262
+ div.innerHTML=`<span class="lt">${esc(l.time||'')}</span><span class="ll ${fk}">${(l.level||'INFO').padEnd(5)}</span><span class="lm${isHL?' hl':''}${isErr?' err':''}">${esc(l.msg||'')}</span>`;
263
+ sc.appendChild(div);
264
+ while(sc.children.length>MAX)sc.removeChild(sc.firstChild);
265
+ if(autoScroll)sc.scrollTop=sc.scrollHeight;
266
+ document.getElementById('log-count').textContent=totalLogs+' 条日志';
267
  }
268
 
269
+ // ── 状态轮询 ──
270
+ async function poll(){
271
+ try{
272
+ const r=await fetch(A+'/api/status');const d=await r.json();
273
+ // 顶栏
274
+ document.getElementById('dot').className='dot-live'+(d.running?'':' off');
275
+ document.getElementById('uptime').textContent=fmtUp(d.uptime_ms||0);
276
+ document.getElementById('ram').textContent=d.ram||'--';
277
+ document.getElementById('pid').textContent=d.pid||'--';
278
+ // 侧栏
279
+ document.getElementById('x-model').textContent=(d.model||'--').split('/').pop().substring(0,30);
280
+ document.getElementById('x-sess').textContent=d.sessions||0;
281
+ document.getElementById('x-plat').textContent=d.platform||'飞书';
282
+ document.getElementById('x-plat-sub').textContent=d.platform_mode||'WebSocket';
283
+ document.getElementById('x-conn').textContent=d.platform_mode||'WebSocket';
284
+ // 配置
285
+ if(d.config){
286
+ document.getElementById('x-appid').textContent=d.config.FEISHU_APP_ID||'--';
287
+ document.getElementById('x-term').textContent=d.config.terminal==='local'?'本地':'远程';
288
+ document.getElementById('x-tz').textContent=d.config.timezone||'Asia/Shanghai';
289
+ document.getElementById('x-turns').textContent=d.config.max_turns||'90';
290
+ document.getElementById('x-comp').textContent=d.config.compress?'已开启':'已关闭';
291
+ document.getElementById('x-comp').style.color=d.config.compress?'var(--g)':'var(--t3)';
292
+ document.getElementById('x-mem').textContent=d.config.memory==='none'?'未配置':'已配置';
293
+ document.getElementById('x-mem').style.color=d.config.memory!=='none'?'var(--g)':'var(--t3)';
294
+ // 模型选择同步
295
+ if(d.model){
296
+ const sel=document.getElementById('msel');
297
+ let found=false;
298
+ for(const o of sel.options)if(o.value===d.model){found=true;break}
299
+ if(!found){const o=document.createElement('option');o.value=d.model;o.textContent=d.model;sel.prepend(o)}
300
+ sel.value=d.model;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  }
302
  }
303
+ document.getElementById('log-time').textContent=new Date().toLocaleTimeString('zh-CN');
304
+ }catch{}
305
  }
306
 
307
+ // ── 会话 ──
308
+ async function loadSess(){
309
+ try{
310
+ const r=await fetch(A+'/api/sessions');const list=await r.json();
311
+ const el=document.getElementById('sess-list');
312
+ if(!list||!list.length){el.innerHTML='<div class="empty-hint">暂无会话</div>';return}
313
+ el.innerHTML=list.slice(0,10).map(s=>`
314
+ <div class="session-item">
315
+ <span class="s-dot${s.active?'':' idle'}"></span>
316
+ <div class="s-info">
317
+ <div class="s-name">${esc(s.name||s.id)}</div>
318
+ <div class="s-meta">${esc(s.platform||'')} · ${s.messages||0} 条 · ${esc(s.last||'')}</div>
 
 
 
 
319
  </div>
320
  </div>`).join('');
321
+ }catch{}
322
  }
323
 
324
+ // ── 操作 ──
325
+ async function doRestart(){
326
+ toast('正在重启网关…','info');
327
+ try{const r=await fetch(A+'/api/restart',{method:'POST'});const d=await r.json();toast(d.ok?'重启成功':'重启失败: '+(d.error||''),'ok' if d.ok else 'err')}catch{toast('请求失败','err')}
 
 
 
 
328
  }
329
+ async function doClear(){
330
+ totalLogs=0;logBuf.length=0;
331
+ document.getElementById('log-scroll').innerHTML='<div class="log-empty">已清除</div>';
332
+ document.getElementById('log-count').textContent='0 条日志';
333
+ toast('日志已清除','ok');
334
  }
335
+ async function doReset(){
336
+ if(!confirm('确定要重置配置吗?所有设置将恢复默认。'))return;
337
+ toast('正在重置…','info');
338
+ try{const r=await fetch(A+'/api/reset',{method:'POST'});const d=await r.json();toast(d.ok?'重置完成':'失败: '+(d.error||''),d.ok?'ok':'err')}catch{toast('请求失败','err')}
339
+ }
340
+ async function doSwitch(){
341
+ const m=document.getElementById('msel').value;
342
+ toast('正在切换模型…','info');
343
+ try{
344
+ const r=await fetch(A+'/api/model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:m})});
345
+ const d=await r.json();
346
+ toast(d.ok?'模型已切换至 '+m.split('/').pop():'切换失败: '+(d.error||''),d.ok?'ok':'err');
347
+ if(d.ok)setTimeout(poll,1000);
348
+ }catch{toast('请求失败','err')}
349
  }
350
 
351
+ // ── 过滤 ──
352
+ function togF(k){
353
+ filters[k]=!filters[k];
354
+ const btn=document.getElementById('f'+k);
355
+ btn.classList.toggle('on',filters[k]);
356
+ document.querySelectorAll('#log-scroll .log-line').forEach(el=>{el.style.display=filters[el.dataset.f]?'':'none'});
 
 
357
  }
358
+ function togAuto(){autoScroll=!autoScroll;document.getElementById('fAuto').classList.toggle('on',autoScroll)}
359
 
360
+ // ── 启动 ──
361
+ poll();setInterval(poll,5000);setInterval(loadSess,30000);loadSess();connectSSE();
 
 
 
 
362
  </script>
363
  </body>
364
  </html>