protocol-proxy 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -1,393 +1,603 @@
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>Protocol Proxy - 协议转换代理管理</title>
7
- <link rel="stylesheet" href="style.css">
8
- </head>
9
- <body>
10
- <script>
11
- // 尽早设置统计日期默认值,避免 init() 时序问题
12
- document.addEventListener('DOMContentLoaded', function() {
13
- var today = new Date().toISOString().slice(0, 10);
14
- var sd = document.getElementById('stats-start-date');
15
- var ed = document.getElementById('stats-end-date');
16
- if (sd && !sd.value) sd.value = today;
17
- if (ed && !ed.value) ed.value = today;
18
- });
19
- </script>
20
- <div class="container">
21
- <button class="theme-toggle" id="theme-toggle" onclick="toggleTheme()" title="切换主题">
22
- <span id="theme-icon">&#9790;</span>
23
- <span id="theme-label" class="theme-label">深色</span>
24
- </button>
25
- <header>
26
- <h1>Protocol Proxy</h1>
27
- <p>OpenAI / Anthropic 协议转换透明代理</p>
28
- </header>
29
-
30
- <div class="stats" id="stats">
31
- <div class="stat-item">
32
- <span class="stat-value" id="stat-total">0</span>
33
- <span class="stat-label">代理配置</span>
34
- </div>
35
- <div class="stat-item">
36
- <span class="stat-value" id="stat-running">0</span>
37
- <span class="stat-label">运行中</span>
38
- </div>
39
- </div>
40
-
41
- <!-- Token 用量统计 -->
42
- <section class="card stats-panel">
43
- <div class="card-header">
44
- <h2>Token 用量统计 <span class="stats-estimated-badge" id="stats-estimated-badge" style="display:none">含估算</span></h2>
45
- <div class="stats-controls">
46
- <div class="model-dropdown" id="stats-proxy-dropdown">
47
- <div class="model-dropdown-trigger stats-filter-trigger" id="stats-proxy-dropdown-trigger">
48
- <span id="stats-proxy-dropdown-value">全部代理</span>
49
- <span class="model-dropdown-arrow">&#9662;</span>
50
- </div>
51
- <div class="model-dropdown-menu" id="stats-proxy-dropdown-menu">
52
- <div class="model-dropdown-options" id="stats-proxy-dropdown-options"></div>
53
- </div>
54
- </div>
55
- <div class="stats-range-btns">
56
- <button class="btn btn-sm stats-range-btn" data-range="hourly">实时</button>
57
- <button class="btn btn-sm stats-range-btn active" data-range="daily">每日</button>
58
- <button class="btn btn-sm stats-range-btn" data-range="monthly">每月</button>
59
- <button class="btn btn-sm stats-range-btn" data-range="yearly">每年</button>
60
- <span class="stats-date-sep">|</span>
61
- <input type="date" id="stats-start-date" class="stats-date-input" placeholder="开始日期">
62
- <span class="stats-date-sep">~</span>
63
- <input type="date" id="stats-end-date" class="stats-date-input" placeholder="结束日期">
64
- </div>
65
- </div>
66
- </div>
67
- <div class="stats-summary" id="stats-summary">
68
- <div class="stats-summary-item">
69
- <span class="stats-summary-value" id="stats-total-tokens">-</span>
70
- <span class="stats-summary-label">总 Token</span>
71
- </div>
72
- <div class="stats-summary-item">
73
- <span class="stats-summary-value" id="stats-prompt-tokens">-</span>
74
- <span class="stats-summary-label">输入 Token</span>
75
- </div>
76
- <div class="stats-summary-item">
77
- <span class="stats-summary-value" id="stats-completion-tokens">-</span>
78
- <span class="stats-summary-label">输出 Token</span>
79
- </div>
80
- <div class="stats-summary-item">
81
- <span class="stats-summary-value" id="stats-total-requests">-</span>
82
- <span class="stats-summary-label">请求数</span>
83
- </div>
84
- </div>
85
- <div id="stats-breakdown" class="stats-breakdown">
86
- <div class="empty">暂无数据</div>
87
- </div>
88
- </section>
89
-
90
- <section class="card">
91
- <div class="card-header">
92
- <h2>代理列表</h2>
93
- <div class="card-header-actions">
94
- <button class="btn" onclick="exportConfig()">导出配置</button>
95
- <button class="btn" onclick="document.getElementById('import-file').click()">导入配置</button>
96
- <input type="file" id="import-file" accept=".json" style="display:none" onchange="handleImportFile(event)">
97
- <button class="btn" onclick="openHistoryViewer()">版本历史</button>
98
- <button class="btn btn-primary" onclick="openModal()">+ 新建代理</button>
99
- </div>
100
- </div>
101
- <div class="proxy-toolbar">
102
- <div class="proxy-search">
103
- <input type="text" id="proxy-search-input" placeholder="搜索代理名称、端口、供应商..." oninput="filterProxies()">
104
- </div>
105
- <div class="proxy-toolbar-actions">
106
- <button class="btn btn-sm" onclick="startAllProxies()">全部启动</button>
107
- <button class="btn btn-sm" onclick="stopAllProxies()">全部停止</button>
108
- <button class="btn btn-sm" onclick="openLogViewer()">日志</button>
109
- </div>
110
- </div>
111
- <div class="provider-health-summary" id="provider-health-summary" style="display:none"></div>
112
- <div id="proxy-list" class="proxy-list">
113
- <div class="empty">加载中...</div>
114
- </div>
115
- </section>
116
-
117
- <!-- 编辑/创建弹窗 -->
118
- <div class="modal" id="modal">
119
- <div class="modal-content">
120
- <div class="modal-header">
121
- <h3 id="modal-title">新建代理</h3>
122
- <button class="btn-close" onclick="closeModal()">&times;</button>
123
- </div>
124
- <form id="proxy-form" onsubmit="handleSubmit(event)">
125
- <input type="hidden" id="proxy-id">
126
-
127
- <div class="form-group">
128
- <label>代理名称</label>
129
- <input type="text" id="proxy-name" required placeholder="例如:OpenAI 代理">
130
- </div>
131
-
132
- <div class="form-row">
133
- <div class="form-group">
134
- <label>监听端口</label>
135
- <input type="number" id="proxy-port" required placeholder="8080" min="1000" max="65535">
136
- </div>
137
- <div class="form-group">
138
- <label>Agent 认证</label>
139
- <input type="hidden" id="proxy-auth" value="false">
140
- <div class="model-dropdown" id="auth-dropdown">
141
- <div class="model-dropdown-trigger" id="auth-dropdown-trigger">
142
- <span id="auth-dropdown-value">不启用</span>
143
- <span class="model-dropdown-arrow">&#9662;</span>
144
- </div>
145
- <div class="model-dropdown-menu" id="auth-dropdown-menu">
146
- <div class="model-dropdown-options" id="auth-dropdown-options">
147
- <div class="model-option selected" data-value="false"><span class="model-option-name">不启用</span></div>
148
- <div class="model-option" data-value="true"><span class="model-option-name">启用</span></div>
149
- </div>
150
- </div>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <div class="form-group" id="auth-token-group" style="display:none">
156
- <label>认证 Token</label>
157
- <input type="text" id="proxy-auth-token" placeholder="Bearer Token">
158
- </div>
159
-
160
- <div class="target-section">
161
- <h4>目标供应商配置</h4>
162
- <div class="target-item">
163
- <input type="hidden" id="provider-id">
164
- <div class="form-row">
165
- <div class="form-group">
166
- <label>供应商</label>
167
- <div class="model-dropdown" id="provider-dropdown">
168
- <div class="model-dropdown-trigger" id="provider-dropdown-trigger">
169
- <span id="provider-dropdown-value">选择供应商...</span>
170
- <span class="model-dropdown-arrow">&#9662;</span>
171
- </div>
172
- <div class="model-dropdown-menu" id="provider-dropdown-menu">
173
- <div class="model-dropdown-options" id="provider-dropdown-options"></div>
174
- <div class="model-add-section">
175
- <input type="text" class="model-add-input" id="provider-add-name" placeholder="供应商名称">
176
- <input type="text" class="model-add-input" id="provider-add-url" placeholder="https://api.example.com">
177
- <button type="button" class="btn btn-primary btn-sm" id="provider-add-btn">添加</button>
178
- </div>
179
- </div>
180
- </div>
181
- </div>
182
- <div class="form-group">
183
- <label>协议</label>
184
- <input type="hidden" id="target-protocol" value="openai">
185
- <div class="model-dropdown" id="protocol-dropdown">
186
- <div class="model-dropdown-trigger" id="protocol-dropdown-trigger">
187
- <span id="protocol-dropdown-value">OpenAI</span>
188
- <span class="model-dropdown-arrow">&#9662;</span>
189
- </div>
190
- <div class="model-dropdown-menu" id="protocol-dropdown-menu">
191
- <div class="model-dropdown-options" id="protocol-dropdown-options">
192
- <div class="model-option selected" data-value="openai"><span class="model-option-name">OpenAI</span></div>
193
- <div class="model-option" data-value="anthropic"><span class="model-option-name">Anthropic</span></div>
194
- <div class="model-option" data-value="gemini"><span class="model-option-name">Gemini</span></div>
195
- </div>
196
- </div>
197
- </div>
198
- </div>
199
- </div>
200
- <div class="form-row">
201
- <div class="form-group">
202
- <label>默认 Model(可选)</label>
203
- <input type="hidden" id="target-model">
204
- <div class="model-dropdown" id="model-dropdown">
205
- <div class="model-dropdown-trigger" id="model-dropdown-trigger">
206
- <span id="model-dropdown-value">选择模型...</span>
207
- <span class="model-dropdown-arrow">&#9662;</span>
208
- </div>
209
- <div class="model-dropdown-menu" id="model-dropdown-menu">
210
- <div class="model-dropdown-options" id="model-dropdown-options"></div>
211
- <div class="model-add-section" id="model-add-section">
212
- <input type="text" class="model-add-input" id="model-add-input" placeholder="输入模型名称">
213
- <button type="button" class="btn btn-primary btn-sm" id="model-add-btn">添加</button>
214
- <button type="button" class="btn btn-sm" id="model-import-btn" onclick="importModels()">自动导入</button>
215
- </div>
216
- </div>
217
- </div>
218
- </div>
219
- </div>
220
- <div class="form-row api-keys-row">
221
- <div class="form-group">
222
- <div class="api-keys-header">
223
- <span>API Keys</span>
224
- <button type="button" id="api-key-add-btn" class="btn btn-sm">+ 添加 Key</button>
225
- </div>
226
- <div id="api-keys-list"></div>
227
- </div>
228
- </div>
229
- <div class="form-row">
230
- <div class="form-group">
231
- <label>路由策略</label>
232
- <input type="hidden" id="routing-strategy" value="primary_fallback">
233
- <div class="model-dropdown" id="routing-dropdown">
234
- <div class="model-dropdown-trigger" id="routing-dropdown-trigger">
235
- <span id="routing-dropdown-value">主备切换</span>
236
- <span class="model-dropdown-arrow">&#9662;</span>
237
- </div>
238
- <div class="model-dropdown-menu" id="routing-dropdown-menu">
239
- <div class="model-dropdown-options" id="routing-dropdown-options">
240
- <div class="model-option selected" data-value="primary_fallback"><span class="model-option-name">主备切换</span></div>
241
- <div class="model-option" data-value="round_robin"><span class="model-option-name">轮询</span></div>
242
- <div class="model-option" data-value="weighted"><span class="model-option-name">加权</span></div>
243
- <div class="model-option" data-value="fastest"><span class="model-option-name">最快优先</span></div>
244
- </div>
245
- </div>
246
- </div>
247
- </div>
248
- <div class="form-group" style="max-width:120px">
249
- <label>主供应商权重</label>
250
- <input type="number" id="provider-weight" min="1" step="1" value="1" placeholder="1">
251
- </div>
252
- <div class="form-group provider-pool-group">
253
- <label>备选供应商</label>
254
- <div class="provider-pool-picker">
255
- <div class="model-dropdown" id="provider-pool-dropdown">
256
- <div class="model-dropdown-trigger" id="provider-pool-dropdown-trigger">
257
- <span id="provider-pool-dropdown-value">从供应商列表添加</span>
258
- <span class="model-dropdown-arrow">&#9662;</span>
259
- </div>
260
- <div class="model-dropdown-menu" id="provider-pool-dropdown-menu">
261
- <div class="model-dropdown-options" id="provider-pool-dropdown-options"></div>
262
- </div>
263
- </div>
264
- </div>
265
- <div class="provider-pool-list" id="provider-pool-list"></div>
266
- </div>
267
- </div>
268
- <div class="form-row" id="azure-fields" style="display:none">
269
- <div class="form-group">
270
- <label>Azure Deployment <span style="color:#64748b;font-size:0.75rem">(仅 Azure 用户填写)</span></label>
271
- <input type="text" id="target-azure-deployment" placeholder="仅 Azure 用户填写">
272
- </div>
273
- <div class="form-group">
274
- <label>Azure API Version <span style="color:#64748b;font-size:0.75rem">(仅 Azure 用户填写)</span></label>
275
- <input type="text" id="target-azure-version" placeholder="仅 Azure 用户填写">
276
- </div>
277
- </div>
278
- </div>
279
- </div>
280
-
281
- <div class="modal-footer">
282
- <button type="button" class="btn" onclick="closeModal()">取消</button>
283
- <button type="button" class="btn" id="test-connection-btn" onclick="testConnection()">测试连接</button>
284
- <button type="submit" class="btn btn-primary">保存</button>
285
- </div>
286
- </form>
287
- </div>
288
- </div>
289
- </div>
290
-
291
- <!-- 确认弹窗 -->
292
- <div class="modal confirm-modal" id="confirm-modal">
293
- <div class="confirm-box">
294
- <div class="confirm-icon" id="confirm-icon">!</div>
295
- <p class="confirm-text" id="confirm-text"></p>
296
- <div class="confirm-actions">
297
- <button class="btn" id="confirm-cancel">取消</button>
298
- <button class="btn btn-danger" id="confirm-ok">删除</button>
299
- </div>
300
- </div>
301
- </div>
302
-
303
- <!-- 测试结果弹窗 -->
304
- <div class="modal confirm-modal" id="test-result-modal">
305
- <div class="confirm-box test-result-box">
306
- <div class="confirm-icon" id="test-result-icon">!</div>
307
- <p class="confirm-text" id="test-result-summary"></p>
308
- <div class="test-result-list" id="test-result-list"></div>
309
- <div class="confirm-actions">
310
- <button class="btn btn-primary" id="test-result-close">知道了</button>
311
- </div>
312
- </div>
313
- </div>
314
-
315
- <!-- 日志查看弹窗 -->
316
- <div class="modal" id="log-modal">
317
- <div class="modal-content" style="max-width:800px">
318
- <div class="modal-header">
319
- <h3>运行日志 <span id="log-total" style="color:#64748b;font-size:0.8rem;font-weight:400"></span></h3>
320
- <button class="btn-close" onclick="closeLogViewer()">&times;</button>
321
- </div>
322
- <div class="log-toolbar">
323
- <select id="log-lines-select" onchange="loadLogs()">
324
- <option value="100">最近 100 行</option>
325
- <option value="200" selected>最近 200 行</option>
326
- <option value="500">最近 500 行</option>
327
- <option value="1000">最近 1000 行</option>
328
- </select>
329
- <button class="btn btn-sm" onclick="loadLogs()">刷新</button>
330
- </div>
331
- <div class="log-content" id="log-content">加载中...</div>
332
- </div>
333
- </div>
334
-
335
- <!-- 版本历史弹窗 -->
336
- <div class="modal" id="history-modal">
337
- <div class="modal-content" style="max-width:600px">
338
- <div class="modal-header">
339
- <h3>配置版本历史</h3>
340
- <button class="btn-close" onclick="closeHistoryViewer()">&times;</button>
341
- </div>
342
- <div class="history-content" id="history-content">加载中...</div>
343
- </div>
344
- </div>
345
-
346
- <!-- 导入预览弹窗 -->
347
- <div class="modal" id="import-modal">
348
- <div class="modal-content" style="max-width:500px">
349
- <div class="modal-header">
350
- <h3>导入配置</h3>
351
- <button class="btn-close" onclick="closeImportModal()">&times;</button>
352
- </div>
353
- <div class="import-preview" id="import-preview">
354
- <div class="import-stats">
355
- <div class="import-stat">
356
- <span class="import-stat-value" id="import-providers-count">0</span>
357
- <span class="import-stat-label">供应商</span>
358
- </div>
359
- <div class="import-stat">
360
- <span class="import-stat-value" id="import-proxies-count">0</span>
361
- <span class="import-stat-label">代理</span>
362
- </div>
363
- </div>
364
- <div class="import-mode">
365
- <label>导入模式</label>
366
- <div class="import-mode-options">
367
- <label class="import-mode-option">
368
- <input type="radio" name="import-mode" value="merge" checked>
369
- <span>
370
- <strong>合并</strong>
371
- <small>按 ID 去重:新增导入项,同 ID 覆盖</small>
372
- </span>
373
- </label>
374
- <label class="import-mode-option">
375
- <input type="radio" name="import-mode" value="overwrite">
376
- <span>
377
- <strong>覆盖</strong>
378
- <small>完全替换现有配置</small>
379
- </span>
380
- </label>
381
- </div>
382
- </div>
383
- </div>
384
- <div class="modal-footer">
385
- <button class="btn" onclick="closeImportModal()">取消</button>
386
- <button class="btn btn-primary" onclick="confirmImport()">确认导入</button>
387
- </div>
388
- </div>
389
- </div>
390
-
391
- <script src="app.js"></script>
392
- </body>
393
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Protocol Proxy</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="style.css">
11
+ </head>
12
+ <body>
13
+ <div class="app">
14
+ <!-- Sidebar -->
15
+ <aside class="sidebar" id="sidebar">
16
+ <div class="sidebar-brand">
17
+ <div class="brand-icon">
18
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
19
+ </div>
20
+ <span class="brand-text">Protocol Proxy</span>
21
+ </div>
22
+ <nav class="sidebar-nav">
23
+ <a href="#" class="nav-item active" data-page="dashboard">
24
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
25
+ <span>总览</span>
26
+ </a>
27
+ <a href="#" class="nav-item" data-page="proxies">
28
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
29
+ <span>代理管理</span>
30
+ <span class="nav-badge" id="nav-proxy-count">0</span>
31
+ </a>
32
+ <a href="#" class="nav-item" data-page="providers">
33
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
34
+ <span>供应商管理</span>
35
+ <span class="nav-badge" id="nav-provider-count">0</span>
36
+ </a>
37
+ <a href="#" class="nav-item" data-page="stats">
38
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>
39
+ <span>用量统计</span>
40
+ </a>
41
+ <a href="#" class="nav-item" data-page="request-logs">
42
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
43
+ <span>请求日志</span>
44
+ </a>
45
+ <a href="#" class="nav-item" data-page="system-logs">
46
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
47
+ <span>系统日志</span>
48
+ </a>
49
+ <a href="#" class="nav-item" data-page="settings">
50
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 5 15.34a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 20.39 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
51
+ <span>设置</span>
52
+ </a>
53
+ </nav>
54
+ <div class="sidebar-footer">
55
+ <button class="theme-toggle-btn" id="theme-toggle-btn" onclick="cycleTheme()">
56
+ <span id="theme-icon">&#9790;</span>
57
+ <span id="theme-label">深色</span>
58
+ </button>
59
+ <div class="version" id="app-version">v2.8.1</div>
60
+ </div>
61
+ </aside>
62
+
63
+ <!-- Main Content -->
64
+ <main class="main">
65
+ <!-- Topbar -->
66
+ <header class="topbar">
67
+ <div class="topbar-left">
68
+ <h1 class="page-title" id="page-title">总览</h1>
69
+ </div>
70
+ <div class="topbar-right">
71
+ <div class="health-summary" id="topbar-health"></div>
72
+ </div>
73
+ </header>
74
+
75
+ <!-- ==================== Dashboard Page ==================== -->
76
+ <div class="page active" id="page-dashboard">
77
+ <div class="dashboard-grid">
78
+ <div class="metric-card">
79
+ <div class="metric-label">运行中代理</div>
80
+ <div class="metric-value" id="dash-running">0</div>
81
+ <div class="metric-sub">/ <span id="dash-total">0</span> 配置</div>
82
+ </div>
83
+ <div class="metric-card">
84
+ <div class="metric-label">今日 Token</div>
85
+ <div class="metric-value" id="dash-tokens">0</div>
86
+ <div class="metric-sub" id="dash-tokens-sub">—</div>
87
+ </div>
88
+ <div class="metric-card">
89
+ <div class="metric-label">今日请求</div>
90
+ <div class="metric-value" id="dash-requests">0</div>
91
+ <div class="metric-sub" id="dash-requests-sub">—</div>
92
+ </div>
93
+ <div class="metric-card">
94
+ <div class="metric-label">供应商健康</div>
95
+ <div class="metric-value" id="dash-health">—</div>
96
+ <div class="metric-sub" id="dash-health-sub">全部正常</div>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="dashboard-row">
101
+ <div class="panel flex-2">
102
+ <div class="panel-header">
103
+ <h3>代理状态</h3>
104
+ <button class="btn btn-sm" onclick="navigateTo('proxies')">查看全部</button>
105
+ </div>
106
+ <div class="proxy-mini-list" id="dash-proxy-list">
107
+ <div class="empty-sm">暂无代理配置</div>
108
+ </div>
109
+ </div>
110
+ <div class="panel flex-1">
111
+ <div class="panel-header">
112
+ <h3>供应商健康</h3>
113
+ </div>
114
+ <div class="provider-health-list" id="dash-provider-health">
115
+ <div class="empty-sm">暂无供应商</div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="panel">
121
+ <div class="panel-header">
122
+ <h3>最近请求</h3>
123
+ <button class="btn btn-sm" onclick="navigateTo('request-logs')">查看全部</button>
124
+ </div>
125
+ <div class="data-table-wrap">
126
+ <table class="data-table">
127
+ <thead>
128
+ <tr>
129
+ <th>时间</th>
130
+ <th>代理</th>
131
+ <th>协议</th>
132
+ <th>模型</th>
133
+ <th>状态</th>
134
+ <th>延迟</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody id="dash-recent-requests">
138
+ <tr><td colspan="6" class="empty-cell">暂无请求记录</td></tr>
139
+ </tbody>
140
+ </table>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- ==================== Proxies Page ==================== -->
146
+ <div class="page" id="page-proxies">
147
+ <div class="page-toolbar">
148
+ <div class="search-box">
149
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
150
+ <input type="text" id="proxy-search" placeholder="搜索代理名称、端口、供应商..." oninput="filterProxies()">
151
+ </div>
152
+ <div class="toolbar-actions">
153
+ <button class="btn btn-sm" onclick="startAllProxies()">全部启动</button>
154
+ <button class="btn btn-sm" onclick="stopAllProxies()">全部停止</button>
155
+ <button class="btn btn-primary" onclick="openProxyModal()">+ 新建代理</button>
156
+ </div>
157
+ </div>
158
+ <div class="proxy-grid" id="proxy-grid">
159
+ <div class="empty-state">
160
+ <div class="empty-icon">
161
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
162
+ </div>
163
+ <p>还没有配置代理</p>
164
+ <button class="btn btn-primary" onclick="openProxyModal()">创建第一个代理</button>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- ==================== Providers Page ==================== -->
170
+ <div class="page" id="page-providers">
171
+ <div class="page-toolbar">
172
+ <div class="search-box">
173
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
174
+ <input type="text" id="provider-search" placeholder="搜索供应商名称、地址..." oninput="filterProviders()">
175
+ </div>
176
+ <div class="toolbar-actions">
177
+ <button class="btn btn-primary" onclick="openProviderModal()">+ 新建供应商</button>
178
+ </div>
179
+ </div>
180
+ <div class="provider-grid" id="provider-grid">
181
+ <div class="empty-state">
182
+ <div class="empty-icon">
183
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
184
+ </div>
185
+ <p>还没有配置供应商</p>
186
+ <button class="btn btn-primary" onclick="openProviderModal()">创建第一个供应商</button>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <!-- ==================== Stats Page ==================== -->
192
+ <div class="page" id="page-stats">
193
+ <div class="page-toolbar">
194
+ <div class="filter-group">
195
+ <select id="stats-range" onchange="changeStatsRange(this.value)">
196
+ <option value="daily">每日</option>
197
+ <option value="monthly">每月</option>
198
+ <option value="yearly">每年</option>
199
+ </select>
200
+ <select id="stats-proxy-filter" onchange="changeStatsProxy(this.value)">
201
+ <option value="">全部代理</option>
202
+ </select>
203
+ <input type="date" id="stats-start" onchange="loadStats()">
204
+ <span class="filter-sep">~</span>
205
+ <input type="date" id="stats-end" onchange="loadStats()">
206
+ </div>
207
+ <div class="toolbar-actions">
208
+ <button class="btn btn-sm" onclick="exportStatsCSV()">导出 CSV</button>
209
+ </div>
210
+ </div>
211
+ <div class="stats-summary-bar" id="stats-summary-bar">
212
+ <div class="stats-summary-item">
213
+ <span class="stats-summary-label">总 Token</span>
214
+ <span class="stats-summary-value" id="stats-total">—</span>
215
+ </div>
216
+ <div class="stats-summary-item">
217
+ <span class="stats-summary-label">输入 Token</span>
218
+ <span class="stats-summary-value" id="stats-prompt">—</span>
219
+ </div>
220
+ <div class="stats-summary-item">
221
+ <span class="stats-summary-label">输出 Token</span>
222
+ <span class="stats-summary-value" id="stats-completion">—</span>
223
+ </div>
224
+ <div class="stats-summary-item">
225
+ <span class="stats-summary-label">请求数</span>
226
+ <span class="stats-summary-value" id="stats-requests">—</span>
227
+ </div>
228
+ </div>
229
+ <div class="panel">
230
+ <div class="panel-header">
231
+ <h3>按模型统计</h3>
232
+ <span class="panel-sub" id="stats-estimated-badge" style="display:none">含估算值</span>
233
+ </div>
234
+ <div class="data-table-wrap">
235
+ <table class="data-table">
236
+ <thead>
237
+ <tr>
238
+ <th>供应商</th>
239
+ <th>模型</th>
240
+ <th class="num">请求数</th>
241
+ <th class="num">输入 Token</th>
242
+ <th class="num">输出 Token</th>
243
+ <th class="num">合计</th>
244
+ </tr>
245
+ </thead>
246
+ <tbody id="stats-table-body">
247
+ <tr><td colspan="6" class="empty-cell">暂无数据</td></tr>
248
+ </tbody>
249
+ </table>
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- ==================== Request Logs Page ==================== -->
255
+ <div class="page" id="page-request-logs">
256
+ <div class="page-toolbar">
257
+ <div class="filter-group">
258
+ <select id="rq-proxy-filter" onchange="filterRequestLogs()">
259
+ <option value="">全部代理</option>
260
+ </select>
261
+ <select id="rq-status-filter" onchange="filterRequestLogs()">
262
+ <option value="">全部状态</option>
263
+ <option value="success">成功</option>
264
+ <option value="failure">失败</option>
265
+ <option value="429">429 限流</option>
266
+ </select>
267
+ <input type="text" id="rq-model-filter" placeholder="模型过滤..." oninput="filterRequestLogs()">
268
+ </div>
269
+ <div class="toolbar-actions">
270
+ <button class="btn btn-sm" id="rq-ws-btn" style="color:var(--text-muted);cursor:default">连接中...</button>
271
+ <button class="btn btn-sm" onclick="exportRequestLogs()">导出</button>
272
+ <button class="btn btn-sm" onclick="clearRequestLogs()">清空</button>
273
+ </div>
274
+ </div>
275
+ <div class="request-log-summary-bar" id="rq-summary"></div>
276
+ <div class="data-table-wrap">
277
+ <table class="data-table">
278
+ <thead>
279
+ <tr>
280
+ <th>时间</th>
281
+ <th>代理</th>
282
+ <th>协议</th>
283
+ <th>模型</th>
284
+ <th>状态</th>
285
+ <th class="num">Tokens</th>
286
+ <th class="num">延迟</th>
287
+ <th>供应商</th>
288
+ <th>Key</th>
289
+ </tr>
290
+ </thead>
291
+ <tbody id="rq-tbody">
292
+ <tr><td colspan="9" class="empty-cell">等待连接...</td></tr>
293
+ </tbody>
294
+ </table>
295
+ </div>
296
+ </div>
297
+
298
+ <!-- ==================== System Logs Page ==================== -->
299
+ <div class="page" id="page-system-logs">
300
+ <div class="page-toolbar">
301
+ <div class="filter-group">
302
+ <select id="log-lines" onchange="loadLogs()">
303
+ <option value="100">最近 100 行</option>
304
+ <option value="200" selected>最近 200 行</option>
305
+ <option value="500">最近 500 行</option>
306
+ <option value="1000">最近 1000 行</option>
307
+ </select>
308
+ <button class="btn btn-sm" onclick="loadLogs()">刷新</button>
309
+ </div>
310
+ </div>
311
+ <div class="log-panel" id="log-content">
312
+ <div class="empty-sm">加载中...</div>
313
+ </div>
314
+ </div>
315
+
316
+ <!-- ==================== Settings Page ==================== -->
317
+ <div class="page" id="page-settings">
318
+ <div class="settings-grid">
319
+ <div class="panel">
320
+ <div class="panel-header">
321
+ <h3>配置管理</h3>
322
+ </div>
323
+ <div class="settings-body">
324
+ <div class="setting-item">
325
+ <div class="setting-info">
326
+ <div class="setting-name">导出配置</div>
327
+ <div class="setting-desc">将所有供应商和代理配置导出为 JSON 文件</div>
328
+ </div>
329
+ <button class="btn btn-sm" onclick="exportConfig()">导出</button>
330
+ </div>
331
+ <div class="setting-item">
332
+ <div class="setting-info">
333
+ <div class="setting-name">导入配置</div>
334
+ <div class="setting-desc">从 JSON 文件导入配置(支持合并或覆盖)</div>
335
+ </div>
336
+ <button class="btn btn-sm" onclick="document.getElementById('import-file').click()">导入</button>
337
+ <input type="file" id="import-file" accept=".json" style="display:none" onchange="handleImportFile(event)">
338
+ </div>
339
+ <div class="setting-item">
340
+ <div class="setting-info">
341
+ <div class="setting-name">版本历史</div>
342
+ <div class="setting-desc">查看配置变更快照,支持回滚到历史版本</div>
343
+ </div>
344
+ <button class="btn btn-sm" onclick="openHistoryModal()">查看</button>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ <div class="panel">
349
+ <div class="panel-header">
350
+ <h3>外观</h3>
351
+ </div>
352
+ <div class="settings-body">
353
+ <div class="setting-item">
354
+ <div class="setting-info">
355
+ <div class="setting-name">主题</div>
356
+ <div class="setting-desc">切换界面配色方案</div>
357
+ </div>
358
+ <select id="settings-theme" onchange="applyTheme(this.value)">
359
+ <option value="dark">深色</option>
360
+ <option value="light">浅色</option>
361
+ <option value="midnight">午夜紫</option>
362
+ <option value="forest">森林绿</option>
363
+ <option value="sunset">日落橙</option>
364
+ <option value="ocean">海洋青</option>
365
+ <option value="sakura">樱花粉</option>
366
+ </select>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </main>
373
+ </div>
374
+
375
+ <!-- ==================== Modals ==================== -->
376
+
377
+ <!-- Proxy Modal -->
378
+ <div class="modal-overlay" id="proxy-modal">
379
+ <div class="modal">
380
+ <div class="modal-header">
381
+ <h3 id="proxy-modal-title">新建代理</h3>
382
+ <button class="modal-close" onclick="closeProxyModal()">&times;</button>
383
+ </div>
384
+ <div class="modal-body">
385
+ <form id="proxy-form" onsubmit="handleProxySubmit(event)">
386
+ <input type="hidden" id="proxy-id">
387
+ <div class="form-row">
388
+ <div class="form-group">
389
+ <label>代理名称</label>
390
+ <input type="text" id="proxy-name" required placeholder="例如:OpenAI 代理">
391
+ </div>
392
+ <div class="form-group" style="max-width:140px">
393
+ <label>监听端口</label>
394
+ <input type="number" id="proxy-port" required placeholder="8080" min="1000" max="65535">
395
+ </div>
396
+ </div>
397
+ <div class="form-group">
398
+ <label>认证</label>
399
+ <select id="proxy-auth">
400
+ <option value="false">不启用</option>
401
+ <option value="true">启用 Bearer Token</option>
402
+ </select>
403
+ </div>
404
+ <div class="form-group" id="proxy-auth-token-group" style="display:none">
405
+ <label>认证 Token</label>
406
+ <input type="text" id="proxy-auth-token" placeholder="Bearer sk-...">
407
+ </div>
408
+ <div class="form-divider"></div>
409
+ <div class="form-section-title">目标供应商</div>
410
+ <div class="form-row">
411
+ <div class="form-group">
412
+ <label>供应商</label>
413
+ <select id="proxy-provider" required>
414
+ <option value="">选择供应商...</option>
415
+ </select>
416
+ </div>
417
+ <div class="form-group">
418
+ <label>默认模型</label>
419
+ <select id="proxy-model">
420
+ <option value="">使用请求模型</option>
421
+ </select>
422
+ </div>
423
+ </div>
424
+ <div class="form-row">
425
+ <div class="form-group">
426
+ <label>路由策略</label>
427
+ <select id="proxy-routing">
428
+ <option value="primary_fallback">主备切换</option>
429
+ <option value="round_robin">轮询</option>
430
+ <option value="weighted">加权随机</option>
431
+ <option value="fastest">最快优先</option>
432
+ </select>
433
+ </div>
434
+ <div class="form-group" style="max-width:120px">
435
+ <label>权重</label>
436
+ <input type="number" id="proxy-weight" min="1" value="1">
437
+ </div>
438
+ </div>
439
+ <div class="form-group">
440
+ <label>备选供应商</label>
441
+ <div class="pool-editor" id="proxy-pool-editor">
442
+ <div class="pool-empty">暂无备选供应商</div>
443
+ </div>
444
+ <button type="button" class="btn btn-sm" onclick="addPoolItem()" style="margin-top:8px">+ 添加备选</button>
445
+ </div>
446
+ <div class="modal-footer">
447
+ <button type="button" class="btn" onclick="closeProxyModal()">取消</button>
448
+ <button type="button" class="btn" onclick="testConnectionFromModal()">测试连接</button>
449
+ <button type="submit" class="btn btn-primary">保存</button>
450
+ </div>
451
+ </form>
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Provider Modal -->
457
+ <div class="modal-overlay" id="provider-modal">
458
+ <div class="modal" style="max-width:640px">
459
+ <div class="modal-header">
460
+ <h3 id="provider-modal-title">新建供应商</h3>
461
+ <button class="modal-close" onclick="closeProviderModal()">&times;</button>
462
+ </div>
463
+ <div class="modal-body">
464
+ <form id="provider-form" onsubmit="handleProviderSubmit(event)">
465
+ <input type="hidden" id="provider-edit-id">
466
+ <div class="form-row">
467
+ <div class="form-group">
468
+ <label>名称</label>
469
+ <input type="text" id="provider-name" required placeholder="例如:OpenAI 官方">
470
+ </div>
471
+ <div class="form-group">
472
+ <label>协议</label>
473
+ <select id="provider-protocol">
474
+ <option value="openai">OpenAI</option>
475
+ <option value="anthropic">Anthropic</option>
476
+ <option value="gemini">Gemini</option>
477
+ </select>
478
+ </div>
479
+ </div>
480
+ <div class="form-group">
481
+ <label>API 地址</label>
482
+ <input type="url" id="provider-url" required placeholder="https://api.openai.com">
483
+ </div>
484
+ <div class="form-group">
485
+ <label>模型列表</label>
486
+ <div class="tag-input" id="provider-models-input">
487
+ <div class="tag-list" id="provider-models-list"></div>
488
+ <input type="text" id="provider-model-input" placeholder="输入模型名称按回车添加..." onkeydown="handleModelTagInput(event)">
489
+ </div>
490
+ <button type="button" class="btn btn-sm" onclick="fetchModelsForProvider(this)" style="margin-top:8px">自动获取模型列表</button>
491
+ </div>
492
+ <div class="form-group">
493
+ <label>API Keys</label>
494
+ <div id="provider-keys-list"></div>
495
+ <button type="button" class="btn btn-sm" onclick="addProviderKey()" style="margin-top:8px">+ 添加 Key</button>
496
+ </div>
497
+ <div class="form-row" id="provider-azure-row" style="display:none">
498
+ <div class="form-group">
499
+ <label>Azure Deployment</label>
500
+ <input type="text" id="provider-azure-deployment" placeholder="仅 Azure 用户填写">
501
+ </div>
502
+ <div class="form-group">
503
+ <label>Azure API Version</label>
504
+ <input type="text" id="provider-azure-version" placeholder="例如:2024-02-01">
505
+ </div>
506
+ </div>
507
+ <div class="modal-footer">
508
+ <button type="button" class="btn" onclick="closeProviderModal()">取消</button>
509
+ <button type="button" class="btn" onclick="testProviderFromModal()">测试连接</button>
510
+ <button type="submit" class="btn btn-primary">保存</button>
511
+ </div>
512
+ </form>
513
+ </div>
514
+ </div>
515
+ </div>
516
+
517
+ <!-- Confirm Modal -->
518
+ <div class="modal-overlay" id="confirm-modal">
519
+ <div class="modal modal-sm">
520
+ <div class="modal-body" style="text-align:center;padding:32px">
521
+ <div class="confirm-icon">!</div>
522
+ <p class="confirm-text" id="confirm-text"></p>
523
+ <div class="modal-footer" style="justify-content:center;border:none;padding:0;margin-top:24px">
524
+ <button class="btn" id="confirm-cancel">取消</button>
525
+ <button class="btn btn-danger" id="confirm-ok">确认</button>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+
531
+ <!-- Test Result Modal -->
532
+ <div class="modal-overlay" id="test-modal">
533
+ <div class="modal modal-sm">
534
+ <div class="modal-header">
535
+ <h3>连接测试结果</h3>
536
+ <button class="modal-close" onclick="closeTestModal()">&times;</button>
537
+ </div>
538
+ <div class="modal-body">
539
+ <p id="test-summary"></p>
540
+ <div id="test-details"></div>
541
+ </div>
542
+ <div class="modal-footer">
543
+ <button class="btn btn-primary" onclick="closeTestModal()">知道了</button>
544
+ </div>
545
+ </div>
546
+ </div>
547
+
548
+ <!-- History Modal -->
549
+ <div class="modal-overlay" id="history-modal">
550
+ <div class="modal" style="max-width:560px">
551
+ <div class="modal-header">
552
+ <h3>配置版本历史</h3>
553
+ <button class="modal-close" onclick="closeHistoryModal()">&times;</button>
554
+ </div>
555
+ <div class="modal-body">
556
+ <div id="history-list">加载中...</div>
557
+ </div>
558
+ </div>
559
+ </div>
560
+
561
+ <!-- Import Modal -->
562
+ <div class="modal-overlay" id="import-modal">
563
+ <div class="modal" style="max-width:480px">
564
+ <div class="modal-header">
565
+ <h3>导入配置</h3>
566
+ <button class="modal-close" onclick="closeImportModal()">&times;</button>
567
+ </div>
568
+ <div class="modal-body">
569
+ <div class="import-stats-row">
570
+ <div class="import-stat">
571
+ <span class="import-stat-value" id="import-providers-count">0</span>
572
+ <span class="import-stat-label">供应商</span>
573
+ </div>
574
+ <div class="import-stat">
575
+ <span class="import-stat-value" id="import-proxies-count">0</span>
576
+ <span class="import-stat-label">代理</span>
577
+ </div>
578
+ </div>
579
+ <div class="form-group">
580
+ <label>导入模式</label>
581
+ <label class="radio-label">
582
+ <input type="radio" name="import-mode" value="merge" checked>
583
+ <span><strong>合并</strong> — 按 ID 去重,新增导入项,同 ID 覆盖</span>
584
+ </label>
585
+ <label class="radio-label">
586
+ <input type="radio" name="import-mode" value="overwrite">
587
+ <span><strong>覆盖</strong> — 完全替换现有配置</span>
588
+ </label>
589
+ </div>
590
+ <div class="modal-footer">
591
+ <button class="btn" onclick="closeImportModal()">取消</button>
592
+ <button class="btn btn-primary" onclick="confirmImport()">确认导入</button>
593
+ </div>
594
+ </div>
595
+ </div>
596
+ </div>
597
+
598
+ <!-- Toast -->
599
+ <div class="toast" id="toast" style="display:none"></div>
600
+
601
+ <script src="app.js"></script>
602
+ </body>
603
+ </html>