@winspan/claude-forge 8.27.0 → 8.28.1

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.
Files changed (53) hide show
  1. package/dist/capability/index.d.ts +9 -0
  2. package/dist/capability/index.d.ts.map +1 -0
  3. package/dist/capability/index.js +9 -0
  4. package/dist/capability/index.js.map +1 -0
  5. package/dist/capability/methodologies/bmad.yaml +69 -0
  6. package/dist/capability/methodologies/harness-engineering.yaml +69 -0
  7. package/dist/capability/methodology-planner.d.ts +33 -0
  8. package/dist/capability/methodology-planner.d.ts.map +1 -0
  9. package/dist/capability/methodology-planner.js +178 -0
  10. package/dist/capability/methodology-planner.js.map +1 -0
  11. package/dist/capability/methodology-registry.d.ts +32 -0
  12. package/dist/capability/methodology-registry.d.ts.map +1 -0
  13. package/dist/capability/methodology-registry.js +97 -0
  14. package/dist/capability/methodology-registry.js.map +1 -0
  15. package/dist/capability/types.d.ts +68 -0
  16. package/dist/capability/types.d.ts.map +1 -0
  17. package/dist/capability/types.js +7 -0
  18. package/dist/capability/types.js.map +1 -0
  19. package/dist/core/storage/schema.sql +40 -0
  20. package/dist/core/storage/sqlite.d.ts +25 -0
  21. package/dist/core/storage/sqlite.d.ts.map +1 -1
  22. package/dist/core/storage/sqlite.js +87 -0
  23. package/dist/core/storage/sqlite.js.map +1 -1
  24. package/dist/daemon/handlers/methodology-formatter.d.ts +9 -0
  25. package/dist/daemon/handlers/methodology-formatter.d.ts.map +1 -0
  26. package/dist/daemon/handlers/methodology-formatter.js +73 -0
  27. package/dist/daemon/handlers/methodology-formatter.js.map +1 -0
  28. package/dist/daemon/handlers/post-tool-use.d.ts +9 -1
  29. package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
  30. package/dist/daemon/handlers/post-tool-use.js +94 -2
  31. package/dist/daemon/handlers/post-tool-use.js.map +1 -1
  32. package/dist/daemon/handlers/pre-tool-use.d.ts.map +1 -1
  33. package/dist/daemon/handlers/pre-tool-use.js +3 -0
  34. package/dist/daemon/handlers/pre-tool-use.js.map +1 -1
  35. package/dist/daemon/handlers/user-prompt.d.ts +5 -1
  36. package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
  37. package/dist/daemon/handlers/user-prompt.js +85 -17
  38. package/dist/daemon/handlers/user-prompt.js.map +1 -1
  39. package/dist/daemon/index.d.ts.map +1 -1
  40. package/dist/daemon/index.js +8 -2
  41. package/dist/daemon/index.js.map +1 -1
  42. package/dist/daemon/routing-state.d.ts +13 -1
  43. package/dist/daemon/routing-state.d.ts.map +1 -1
  44. package/dist/daemon/routing-state.js +154 -4
  45. package/dist/daemon/routing-state.js.map +1 -1
  46. package/dist/web/server.d.ts.map +1 -1
  47. package/dist/web/server.js +123 -2
  48. package/dist/web/server.js.map +1 -1
  49. package/dist/web/static/assets/index-CtylfoaN.css +1 -0
  50. package/dist/web/static/assets/index-DnaQt27h.js +388 -0
  51. package/dist/web/static/assets/index-DnaQt27h.js.map +1 -0
  52. package/dist/web/static/index.html +12 -2971
  53. package/package.json +5 -2
@@ -1,2973 +1,14 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
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>Claude Forge 管理后台</title>
7
- <script src="vendor/chart.umd.min.js"></script>
8
- <!-- Monaco Editor CDN (Phase 4 Feature 1) -->
9
- <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
10
- <style>
11
- /* === Reset & Theme === */
12
- * { margin: 0; padding: 0; box-sizing: border-box; }
13
- :root {
14
- --bg: #f7f8fa;
15
- --bg-card: #ffffff;
16
- --bg-hover: #f1f3f7;
17
- --bg-secondary: #f7f8fa;
18
- --border: #e4e7ec;
19
- --border-hover: #cfd4dc;
20
- --text: #111827;
21
- --text-muted: #4b5563;
22
- --text-dim: #9ca3af;
23
- --primary: #4f46e5;
24
- --primary-hover: #4338ca;
25
- --primary-soft: #eef2ff;
26
- --primary-border: #c7d2fe;
27
- --green: #16a34a; --green-soft: #dcfce7;
28
- --yellow: #d97706; --yellow-soft: #fef3c7;
29
- --red: #dc2626; --red-soft: #fee2e2;
30
- --blue: #2563eb; --blue-soft: #dbeafe;
31
- --purple: #7c3aed; --purple-soft: #ede9fe;
32
- --radius: 10px;
33
- --radius-sm: 6px;
34
- --shadow-sm: 0 1px 2px rgba(16,24,40,0.04);
35
- --shadow: 0 1px 3px rgba(16,24,40,0.06), 0 1px 2px rgba(16,24,40,0.04);
36
- --shadow-lg: 0 4px 12px rgba(16,24,40,0.08);
37
- --sidebar-width: 240px;
38
- --drawer-width: 520px;
39
- }
40
- html, body { height: 100%; }
41
- body {
42
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
43
- background: var(--bg);
44
- color: var(--text);
45
- line-height: 1.55;
46
- -webkit-font-smoothing: antialiased;
47
- }
48
-
49
- /* === Layout === */
50
- .app { display: flex; min-height: 100vh; position: relative; }
51
-
52
- /* Sidebar */
53
- .sidebar {
54
- width: var(--sidebar-width);
55
- background: var(--bg-card);
56
- border-right: 1px solid var(--border);
57
- padding: 1.25rem 0.75rem;
58
- position: fixed; top: 0; left: 0; bottom: 0;
59
- display: flex; flex-direction: column;
60
- overflow-y: auto;
61
- z-index: 100;
62
- }
63
- .sidebar .brand {
64
- display: flex; align-items: center; gap: 0.625rem;
65
- padding: 0.5rem 0.75rem 1.25rem;
66
- border-bottom: 1px solid var(--border);
67
- margin-bottom: 1rem;
68
- }
69
- .sidebar .brand-icon {
70
- width: 32px; height: 32px; border-radius: 8px;
71
- background: linear-gradient(135deg, var(--primary), var(--purple));
72
- display: flex; align-items: center; justify-content: center;
73
- color: white; font-weight: 700; font-size: 0.95rem;
74
- box-shadow: var(--shadow);
75
- }
76
- .sidebar .brand-text {
77
- font-weight: 600; font-size: 0.95rem;
78
- color: var(--text); letter-spacing: -0.01em;
79
- }
80
- .sidebar .nav-section-title {
81
- font-size: 0.7rem; font-weight: 600; color: var(--text-dim);
82
- text-transform: uppercase; letter-spacing: 0.06em;
83
- padding: 0.75rem 0.875rem 0.375rem;
84
- }
85
- .sidebar a {
86
- display: flex; align-items: center; gap: 0.625rem;
87
- padding: 0.5rem 0.875rem; margin: 1px 0;
88
- color: var(--text-muted); text-decoration: none;
89
- border-radius: var(--radius-sm); cursor: pointer;
90
- font-size: 0.875rem; font-weight: 500;
91
- transition: background 0.15s, color 0.15s;
92
- }
93
- .sidebar a:hover { background: var(--bg-hover); color: var(--text); }
94
- .sidebar a.active {
95
- background: var(--primary-soft);
96
- color: var(--primary);
97
- }
98
- .nav-icon { width: 18px; height: 18px; flex-shrink: 0; }
99
-
100
- /* Main content */
101
- .main {
102
- flex: 1;
103
- margin-left: var(--sidebar-width);
104
- min-height: 100vh;
105
- }
106
- .topbar {
107
- height: 60px;
108
- background: var(--bg-card);
109
- border-bottom: 1px solid var(--border);
110
- display: flex;
111
- align-items: center;
112
- justify-content: space-between;
113
- padding: 0 1.75rem;
114
- position: sticky;
115
- top: 0;
116
- z-index: 50;
117
- }
118
- .topbar .page-title {
119
- font-size: 1rem;
120
- font-weight: 600;
121
- color: var(--text);
122
- }
123
- .topbar .actions {
124
- display: flex;
125
- gap: 0.5rem;
126
- align-items: center;
127
- }
128
- .container {
129
- padding: 1.5rem 1.75rem;
130
- max-width: 1400px;
131
- }
132
- .page {
133
- display: none;
134
- }
135
- .page.active {
136
- display: block;
137
- }
138
-
139
- /* Drawer (侧边抽屉) */
140
- .drawer-overlay {
141
- position: fixed;
142
- top: 0;
143
- left: 0;
144
- right: 0;
145
- bottom: 0;
146
- background: rgba(0, 0, 0, 0.4);
147
- z-index: 200;
148
- opacity: 0;
149
- visibility: hidden;
150
- transition: opacity 0.2s, visibility 0.2s;
151
- }
152
- .drawer-overlay.active {
153
- opacity: 1;
154
- visibility: visible;
155
- }
156
- .drawer {
157
- position: fixed;
158
- top: 0;
159
- right: 0;
160
- bottom: 0;
161
- width: var(--drawer-width);
162
- background: var(--bg-card);
163
- border-left: 1px solid var(--border);
164
- box-shadow: var(--shadow-lg);
165
- z-index: 201;
166
- display: flex;
167
- flex-direction: column;
168
- transform: translateX(100%);
169
- transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
170
- }
171
- .drawer.active {
172
- transform: translateX(0);
173
- }
174
- .drawer-header {
175
- display: flex;
176
- align-items: center;
177
- justify-content: space-between;
178
- padding: 1.25rem 1.5rem;
179
- border-bottom: 1px solid var(--border);
180
- flex-shrink: 0;
181
- }
182
- .drawer-title {
183
- font-size: 1rem;
184
- font-weight: 600;
185
- color: var(--text);
186
- }
187
- .drawer-close {
188
- width: 32px; height: 32px;
189
- border-radius: var(--radius-sm);
190
- border: 1px solid var(--border);
191
- background: var(--bg-card);
192
- cursor: pointer;
193
- display: flex; align-items: center; justify-content: center;
194
- color: var(--text-muted);
195
- transition: all 0.15s;
196
- }
197
- .drawer-close:hover {
198
- background: var(--bg-hover);
199
- color: var(--text);
200
- }
201
- .drawer-body {
202
- flex: 1;
203
- overflow-y: auto;
204
- padding: 1.5rem;
205
- }
206
-
207
- /* === Typography === */
208
- h1 { font-size: 1.375rem; font-weight: 600; margin-bottom: 1.25rem; letter-spacing: -0.02em; }
209
- h2 { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; }
210
- h3 { font-size: 0.9rem; font-weight: 600; }
211
-
212
- /* === Stat cards === */
213
- .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
214
- .stat-card {
215
- background: var(--bg-card);
216
- border: 1px solid var(--border);
217
- border-radius: var(--radius);
218
- padding: 1.125rem 1.25rem;
219
- box-shadow: var(--shadow-sm);
220
- }
221
- .stat-card .label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500; }
222
- .stat-card .value { font-size: 1.75rem; font-weight: 700; margin-top: 0.25rem; color: var(--text); letter-spacing: -0.02em; }
223
-
224
- /* === Grid layouts === */
225
- .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
226
- .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.25rem; }
227
- @media (max-width: 900px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }
228
-
229
- /* === Panel (white box) === */
230
- .panel {
231
- background: var(--bg-card);
232
- border: 1px solid var(--border);
233
- border-radius: var(--radius);
234
- box-shadow: var(--shadow-sm);
235
- overflow: hidden;
236
- }
237
- .panel-header {
238
- display: flex; align-items: center; justify-content: space-between;
239
- padding: 1rem 1.25rem;
240
- border-bottom: 1px solid var(--border);
241
- }
242
- .panel-title { font-size: 0.875rem; font-weight: 600; color: var(--text); }
243
- .panel-body { padding: 1.25rem; }
244
-
245
- /* === Table === */
246
- table { width: 100%; border-collapse: collapse; }
247
- th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
248
- th { background: var(--bg-secondary); color: var(--text-muted); font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
249
- td { font-size: 0.875rem; color: var(--text); }
250
- tr:last-child td { border-bottom: none; }
251
- tr:hover td { background: var(--bg-hover); cursor: pointer; }
252
-
253
- /* === Badges === */
254
- .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 0.7rem; font-weight: 500; }
255
- .badge-block { background: var(--red-soft); color: var(--red); }
256
- .badge-warn { background: var(--yellow-soft); color: var(--yellow); }
257
- .badge-allow { background: var(--green-soft); color: var(--green); }
258
- .badge-info { background: var(--primary-soft); color: var(--primary); }
259
- .badge-live { background: var(--red-soft); color: var(--red); animation: pulse 1.5s infinite; }
260
- @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
261
-
262
- /* === Buttons === */
263
- .btn { background: var(--bg-card); color: var(--text-muted); border: 1px solid var(--border); padding: 0.45rem 0.875rem; border-radius: var(--radius-sm); cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s; }
264
- .btn:hover { border-color: var(--border-hover); color: var(--text); background: var(--bg-hover); }
265
- .btn-primary { background: var(--primary); color: white; border-color: var(--primary); }
266
- .btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); color: white; }
267
- select.btn { appearance: none; padding-right: 1.75rem; 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='%234b5563' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.6rem center; }
268
-
269
- /* === Search === */
270
- .search-box { padding: 0.55rem 0.875rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); font-size: 0.875rem; transition: border-color 0.15s, box-shadow 0.15s; min-width: 240px; }
271
- .search-box::placeholder { color: var(--text-dim); }
272
- .search-box:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.12); }
273
-
274
- /* === Toolbar (above tables/lists) === */
275
- .toolbar { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; }
276
-
277
- /* === List items === */
278
- .list-item {
279
- display: flex; align-items: center; justify-content: space-between;
280
- padding: 0.875rem 1.25rem;
281
- border-bottom: 1px solid var(--border);
282
- cursor: pointer;
283
- transition: background 0.12s;
284
- }
285
- .list-item:last-child { border-bottom: none; }
286
- .list-item:hover { background: var(--bg-hover); }
287
- .list-item.selected { background: var(--primary-soft); }
288
-
289
- /* === Empty state === */
290
- .empty { text-align: center; padding: 3rem 1rem; color: var(--text-dim); }
291
- .empty-icon { font-size: 2rem; margin-bottom: 0.75rem; }
292
- .empty-text { font-size: 0.875rem; }
293
-
294
- /* === Loading === */
295
- .loading { text-align: center; padding: 2rem; color: var(--text-dim); font-size: 0.875rem; }
296
-
297
- /* === Tags === */
298
- .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; background: var(--bg-secondary); color: var(--text-muted); border: 1px solid var(--border); margin-right: 0.25rem; }
299
-
300
- /* === Detail sections in drawer === */
301
- .detail-section { margin-bottom: 1.5rem; }
302
- .detail-label { font-size: 0.75rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.375rem; }
303
- .detail-value { font-size: 0.875rem; color: var(--text); }
304
- pre.detail-code { font-family: ui-monospace, monospace; font-size: 0.8rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.75rem; overflow-x: auto; white-space: pre-wrap; }
305
-
306
- /* === Live log === */
307
- .live-log { font-family: ui-monospace, monospace; font-size: 0.8rem; max-height: 65vh; overflow-y: auto; background: #0f172a; color: #e2e8f0; border-radius: var(--radius); padding: 1rem; }
308
- .live-log-line { padding: 2px 0; border-bottom: 1px solid #1e293b; }
309
-
310
- /* === Chart container === */
311
- .chart-wrap { position: relative; height: 220px; }
312
-
313
- /* === Animations === */
314
- @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
315
- .fade-in { animation: fadeIn 0.2s ease-out; }
316
-
317
- /* === Activity feed === */
318
- .activity-item { display: flex; gap: 0.75rem; padding: 0.625rem 0; border-bottom: 1px solid var(--border); }
319
- .activity-item:last-child { border-bottom: none; }
320
- .activity-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
321
- .activity-dot-green { background: var(--green); }
322
- .activity-dot-blue { background: var(--blue); }
323
- .activity-dot-yellow { background: var(--yellow); }
324
- .activity-dot-red { background: var(--red); }
325
- .activity-content { flex: 1; min-width: 0; }
326
- .activity-title { font-size: 0.8125rem; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
327
- .activity-time { font-size: 0.75rem; color: var(--text-dim); margin-top: 1px; }
328
- </style>
329
- </head>
330
- <body>
331
- <div class="app">
332
- <!-- Sidebar -->
333
- <aside class="sidebar">
334
- <div class="brand">
335
- <div class="brand-icon">CF</div>
336
- <div class="brand-text">Claude Forge</div>
337
- </div>
338
- <div class="nav-section-title">总览</div>
339
- <a onclick="nav('dashboard')" id="nav-dashboard" class="active">
340
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
341
- 仪表盘
342
- </a>
343
- <div class="nav-section-title">活动</div>
344
- <a onclick="nav('sessions')" id="nav-sessions">
345
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
346
- 会话
347
- </a>
348
- <a onclick="nav('events')" id="nav-events">
349
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
350
- 事件
351
- </a>
352
- <a onclick="nav('injections')" id="nav-injections">
353
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
354
- 注入
355
- </a>
356
- <a onclick="nav('execution-trace')" id="nav-execution-trace">
357
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
358
- 执行追踪
359
- </a>
360
- <div class="nav-section-title">配置</div>
361
- <a onclick="nav('agents')" id="nav-agents">
362
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
363
- Agent 管理
364
- </a>
365
- <a onclick="nav('skills')" id="nav-skills">
366
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
367
- Skill 管理
368
- </a>
369
- <a onclick="nav('ai-config')" id="nav-ai-config">
370
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6M5.6 5.6l4.2 4.2m4.2 4.2l4.2 4.2M1 12h6m6 0h6M5.6 18.4l4.2-4.2m4.2-4.2l4.2-4.2"/></svg>
371
- AI 配置
372
- </a>
373
- <a onclick="nav('routing')" id="nav-routing">
374
- <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M9 6h9M6 9v9"/></svg>
375
- Agent 路由
376
- </a>
377
- </aside>
378
-
379
- <!-- Main -->
380
- <main class="main">
381
- <div class="topbar">
382
- <div class="page-title" id="topbar-title">仪表盘</div>
383
- <div class="actions">
384
- <span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem">检查中...</span>
385
- <button class="btn" onclick="refreshPage()">↻ 刷新</button>
386
- </div>
387
- </div>
388
- <div class="container">
389
-
390
- <!-- Dashboard -->
391
- <div id="page-dashboard" class="page active">
392
- <div class="cards" id="dash-stats"></div>
393
- <div class="grid-2" style="margin-bottom:1.25rem">
394
- <div class="panel">
395
- <div class="panel-header">
396
- <span class="panel-title">近 7 天活动</span>
397
- </div>
398
- <div class="panel-body"><div class="chart-wrap"><canvas id="chart-activity"></canvas></div></div>
399
- </div>
400
- <div class="panel">
401
- <div class="panel-header">
402
- <span class="panel-title">工具使用分布</span>
403
- </div>
404
- <div class="panel-body"><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
405
- </div>
406
- </div>
407
- <div class="grid-2">
408
- <div class="panel">
409
- <div class="panel-header">
410
- <span class="panel-title">最近会话</span>
411
- <a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer">查看全部 →</a>
412
- </div>
413
- <div id="dash-sessions"></div>
414
- </div>
415
- <div class="panel">
416
- <div class="panel-header">
417
- <span class="panel-title">最近活动</span>
418
- </div>
419
- <div class="panel-body" id="dash-activity"></div>
420
- </div>
421
- </div>
422
- </div>
423
-
424
- <!-- Sessions -->
425
- <div id="page-sessions" class="page">
426
- <div class="toolbar">
427
- <input class="search-box" id="sessions-search" placeholder="搜索会话..." oninput="filterSessions()">
428
- </div>
429
- <div class="panel" id="sessions-panel">
430
- <div id="sessions-list"></div>
431
- </div>
432
- </div>
433
-
434
- <!-- Events -->
435
- <div id="page-events" class="page">
436
- <div class="toolbar">
437
- <input class="search-box" id="events-search" placeholder="按类型、工具、项目搜索..." oninput="filterEvents()">
438
- <select class="btn" id="events-type" onchange="filterEvents()">
439
- <option value="">全部类型</option>
440
- <option value="PreToolUse">PreToolUse</option>
441
- <option value="PostToolUse">PostToolUse</option>
442
- <option value="UserPromptSubmit">UserPromptSubmit</option>
443
- <option value="Stop">Stop</option>
444
- </select>
445
- </div>
446
- <div class="panel">
447
- <table>
448
- <thead><tr><th>时间</th><th>类型</th><th>工具</th><th>项目</th></tr></thead>
449
- <tbody id="events-body"></tbody>
450
- </table>
451
- </div>
452
- </div>
453
-
454
- <!-- Injections -->
455
- <div id="page-injections" class="page">
456
- <div class="toolbar">
457
- <input class="search-box" id="inj-search" placeholder="搜索注入内容..." oninput="filterInjections()">
458
- <select class="btn" id="inj-handler" onchange="filterInjections()">
459
- <option value="">全部 Handler</option>
460
- <option value="UserPromptSubmitHandler">UserPromptSubmit</option>
461
- <option value="PreToolUseHandler">PreToolUse</option>
462
- <option value="PostToolUseHandler">PostToolUse</option>
463
- </select>
464
- </div>
465
- <div class="panel" id="inj-panel">
466
- <div id="inj-list"></div>
467
- </div>
468
- </div>
469
-
470
- <!-- AI Config -->
471
- <div id="page-ai-config" class="page">
472
- <div class="panel">
473
- <div class="panel-header">
474
- <span class="panel-title">AI 配置</span>
475
- <div style="display:flex;gap:0.5rem">
476
- <button class="btn" onclick="testAIConnection()">🔌 测试连接</button>
477
- <button class="btn btn-primary" onclick="saveAIConfig()">💾 保存</button>
478
- </div>
479
- </div>
480
- <div class="panel-body">
481
- <div style="max-width:600px">
482
- <div style="margin-bottom:1rem">
483
- <label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">API Key</label>
484
- <div style="display:flex;gap:0.5rem">
485
- <input type="password" id="ai-api-key" class="search-box" style="flex:1" placeholder="sk-...">
486
- <button class="btn" onclick="toggleAPIKeyVisibility()" id="toggle-api-key-btn">👁️ 显示</button>
487
- </div>
488
- </div>
489
- <div style="margin-bottom:1rem">
490
- <label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">Base URL</label>
491
- <input type="text" id="ai-base-url" class="search-box" placeholder="https://api.anthropic.com">
492
- </div>
493
- <div style="margin-bottom:1rem">
494
- <label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">Provider</label>
495
- <input type="text" id="ai-provider" class="search-box" value="anthropic" readonly style="background:var(--bg-secondary)">
496
- </div>
497
- <div style="margin-bottom:1rem">
498
- <label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">Model</label>
499
- <div style="display:flex;gap:0.5rem">
500
- <select id="ai-model" class="btn" style="flex:1">
501
- <option value="">加载中...</option>
502
- </select>
503
- <button class="btn" onclick="refreshAIModels()">↻ 刷新模型列表</button>
504
- </div>
505
- <div style="margin-top:0.25rem;font-size:0.75rem;color:var(--text-dim)">主模型:用于 Distiller(会话提炼)和 QualityGate(质量检查)</div>
506
- </div>
507
- <div style="margin-bottom:1rem">
508
- <label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">意图分类模型</label>
509
- <select id="ai-classifier-model" class="btn" style="width:100%">
510
- <option value="">(与主模型相同)</option>
511
- </select>
512
- <div style="margin-top:0.25rem;font-size:0.75rem;color:var(--text-dim)">用于 UserPromptSubmit 意图识别。建议选 haiku 类小模型(更快,10s 内完成)</div>
513
- </div>
514
- <div style="margin-bottom:1rem">
515
- <label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">分类超时(毫秒)</label>
516
- <input type="number" id="ai-classifier-timeout" class="search-box" placeholder="10000" min="1000" max="60000" step="1000">
517
- <div style="margin-top:0.25rem;font-size:0.75rem;color:var(--text-dim)">超过此时间会降级为正则兜底。默认 10000ms</div>
518
- </div>
519
- <div id="ai-config-success" style="margin-top:1rem;padding:0.75rem;background:var(--primary);color:white;border-radius:var(--radius-sm);display:none"></div>
520
- <div id="ai-config-error" style="margin-top:1rem;padding:0.75rem;background:var(--red);color:white;border-radius:var(--radius-sm);display:none"></div>
521
- <div style="margin-top:1rem;padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);font-size:0.875rem;color:var(--text-muted)">
522
- <strong>提示:</strong>配置保存后需要重启 daemon 才能生效。<br>
523
- 运行 <code style="background:var(--bg-card);padding:0.125rem 0.375rem;border-radius:4px">cf daemon stop && cf daemon start</code>
524
- </div>
525
- </div>
526
- </div>
527
- </div>
528
- </div>
529
-
530
- <!-- Execution Trace -->
531
- <div id="page-execution-trace" class="page">
532
- <div class="toolbar">
533
- <input class="search-box" id="trace-search" placeholder="搜索 prompt..." oninput="filterTraces()">
534
- <select class="btn" id="trace-agent-filter" onchange="loadExecutionTraces()">
535
- <option value="">全部 Agent/Skill</option>
536
- </select>
537
- <select class="btn" id="trace-obeyed-filter" onchange="loadExecutionTraces()">
538
- <option value="">全部状态</option>
539
- <option value="true">✓ 已遵守</option>
540
- <option value="false">✗ 违抗</option>
541
- </select>
542
- <button class="btn" onclick="loadExecutionTraces()">↻ 刷新</button>
543
- </div>
544
- <div class="panel" style="margin-bottom:1rem">
545
- <div class="panel-header"><span class="panel-title">实时状态</span></div>
546
- <div class="panel-body">
547
- <div id="trace-live-status" style="max-height:200px;overflow-y:auto;font-size:0.875rem"></div>
548
- </div>
549
- </div>
550
- <div class="panel">
551
- <div class="panel-body">
552
- <div id="trace-list"></div>
553
- </div>
554
- </div>
555
- </div>
556
-
557
- <!-- Agent Management -->
558
- <div id="page-agents" class="page">
559
- <div class="panel">
560
- <div class="panel-header">
561
- <span class="panel-title">Agent 管理</span>
562
- <button class="btn" onclick="loadAgents()">↻ 刷新</button>
563
- </div>
564
- <div class="panel-body">
565
- <table>
566
- <thead>
567
- <tr>
568
- <th>名称</th>
569
- <th>描述</th>
570
- <th>分类</th>
571
- <th>版本</th>
572
- <th>来源</th>
573
- <th>操作</th>
574
- </tr>
575
- </thead>
576
- <tbody id="agents-body"></tbody>
577
- </table>
578
- </div>
579
- </div>
580
- </div>
581
-
582
- <!-- Skill Management -->
583
- <div id="page-skills" class="page">
584
- <div class="panel">
585
- <div class="panel-header">
586
- <span class="panel-title">Skill 管理</span>
587
- <button class="btn" onclick="loadSkills()">↻ 刷新</button>
588
- </div>
589
- <div class="panel-body">
590
- <table>
591
- <thead>
592
- <tr>
593
- <th>名称</th>
594
- <th>描述</th>
595
- <th>版本</th>
596
- <th>来源</th>
597
- <th>操作</th>
598
- </tr>
599
- </thead>
600
- <tbody id="skills-body"></tbody>
601
- </table>
602
- </div>
603
- </div>
604
- </div>
605
-
606
- <!-- Agent Routing -->
607
- <div id="page-routing" class="page">
608
- <div class="toolbar" style="display:flex;gap:0.5rem;align-items:center">
609
- <div style="display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden">
610
- <button class="btn" id="routing-tab-overview" onclick="routingTab('overview')" style="border-radius:0;border:none">总览</button>
611
- <button class="btn" id="routing-tab-events" onclick="routingTab('events')" style="border-radius:0;border:none">决策明细</button>
612
- <button class="btn" id="routing-tab-refusals" onclick="routingTab('refusals')" style="border-radius:0;border:none">违抗聚合</button>
613
- <button class="btn" id="routing-tab-performance" onclick="routingTab('performance')" style="border-radius:0;border:none">性能分析</button>
614
- <button class="btn" id="routing-tab-ai-optimization" onclick="routingTab('ai-optimization')" style="border-radius:0;border:none">AI 优化</button>
615
- <button class="btn" id="routing-tab-editor" onclick="routingTab('editor')" style="border-radius:0;border:none">路由调优</button>
616
- <button class="btn" id="routing-tab-experiments" onclick="routingTab('experiments')" style="border-radius:0;border:none">A/B 测试</button>
617
- <button class="btn" id="routing-tab-recommendations" onclick="routingTab('recommendations')" style="border-radius:0;border:none">规则推荐</button>
618
- </div>
619
- <div style="flex:1"></div>
620
- <select class="btn" id="routing-window" onchange="loadRouting()" style="display:none">
621
- <option value="24">近 24 小时</option>
622
- <option value="168" selected>近 7 天</option>
623
- <option value="720">近 30 天</option>
624
- </select>
625
- </div>
626
-
627
- <!-- Overview subpanel -->
628
- <div id="routing-sub-overview">
629
- <div class="cards" id="routing-cards"></div>
630
- <div class="grid-2" style="margin-top:1.25rem">
631
- <div class="panel">
632
- <div class="panel-header"><span class="panel-title">按 Agent 听话率</span></div>
633
- <div class="panel-body" id="routing-by-agent"></div>
634
- </div>
635
- <div class="panel">
636
- <div class="panel-header"><span class="panel-title">注入话术版本 A/B</span></div>
637
- <div class="panel-body" id="routing-by-version"></div>
638
- </div>
639
- </div>
640
- </div>
641
-
642
- <!-- Events subpanel -->
643
- <div id="routing-sub-events" style="display:none">
644
- <div class="toolbar" style="display:flex;gap:0.5rem">
645
- <select class="btn" id="routing-filter-obeyed" onchange="loadRoutingEvents()">
646
- <option value="">全部状态</option>
647
- <option value="forced">已强路由</option>
648
- <option value="1">遵守 (obeyed)</option>
649
- <option value="0">违抗 (refused)</option>
650
- <option value="null">待判定/未路由</option>
651
- </select>
652
- <input class="search-box" id="routing-filter-agent" placeholder="按 agent 名过滤..." oninput="loadRoutingEvents()">
653
- </div>
654
- <div class="panel">
655
- <table>
656
- <thead><tr><th>时间</th><th>Agent</th><th>状态</th><th>首个工具</th><th>Prompt 摘要</th></tr></thead>
657
- <tbody id="routing-events-tbody"></tbody>
658
- </table>
659
- </div>
660
- </div>
661
-
662
- <!-- Refusals subpanel -->
663
- <div id="routing-sub-refusals" style="display:none">
664
- <div class="panel">
665
- <div class="panel-header"><span class="panel-title">违抗聚合 (taskType × agent)</span></div>
666
- <div class="panel-body" id="routing-refusals"></div>
667
- </div>
668
- </div>
669
-
670
- <!-- Performance subpanel -->
671
- <div id="routing-sub-performance" style="display:none">
672
- <div class="cards" id="routing-performance-cards"></div>
673
- <div class="grid-2" style="margin-top:1.25rem">
674
- <div class="panel">
675
- <div class="panel-header"><span class="panel-title">每日路由趋势</span></div>
676
- <div class="panel-body"><div class="chart-wrap"><canvas id="chart-routing-trend"></canvas></div></div>
677
- </div>
678
- <div class="panel">
679
- <div class="panel-header"><span class="panel-title">平均真实执行耗时趋势</span></div>
680
- <div class="panel-body"><div class="chart-wrap"><canvas id="chart-routing-latency"></canvas></div></div>
681
- </div>
682
- </div>
683
- <div class="grid-2" style="margin-top:1.25rem">
684
- <div class="panel">
685
- <div class="panel-header"><span class="panel-title">Per-agent 指标</span></div>
686
- <div class="panel-body">
687
- <table>
688
- <thead><tr><th>Agent</th><th>总次数</th><th>已判定</th><th>遵守率</th><th>违抗率</th><th>分类耗时</th><th>真实耗时</th></tr></thead>
689
- <tbody id="routing-performance-by-agent"></tbody>
690
- </table>
691
- </div>
692
- </div>
693
- <div class="panel">
694
- <div class="panel-header"><span class="panel-title">高违抗率榜单</span></div>
695
- <div class="panel-body" id="routing-high-refusal"></div>
696
- </div>
697
- </div>
698
- </div>
699
-
700
- <!-- AI optimization subpanel -->
701
- <div id="routing-sub-ai-optimization" style="display:none">
702
- <div class="panel">
703
- <div class="panel-header">
704
- <span class="panel-title">AI 优化建议</span>
705
- <div style="display:flex;gap:0.5rem;align-items:center">
706
- <label style="font-size:0.875rem;color:var(--text-dim)">最小样本
707
- <input id="ai-opt-min-attempts" type="number" value="10" min="1" max="100" style="width:72px;margin-left:0.25rem" class="btn">
708
- </label>
709
- <button class="btn" onclick="loadRoutingAIOptimization()">↻ 生成建议</button>
710
- </div>
711
- </div>
712
- <div class="panel-body">
713
- <div style="margin-bottom:0.75rem;font-size:0.875rem;color:var(--text-dim)">
714
- 基于高违抗率 agent、规则推荐、执行追踪证据,由 AI 生成针对 agent / skill / routing rule 的优化建议。
715
- </div>
716
- <div id="routing-ai-optimization"></div>
717
- </div>
718
- </div>
719
- </div>
720
-
721
- <!-- Editor subpanel (Phase 3 Feature 2, upgraded to Monaco in Phase 4 Feature 1) -->
722
- <div id="routing-sub-editor" style="display:none">
723
- <div class="panel">
724
- <div class="panel-header">
725
- <span class="panel-title">路由配置编辑器</span>
726
- <div style="display:flex;gap:0.5rem">
727
- <button class="btn" onclick="loadRoutingConfig()">↻ 重新加载</button>
728
- <button class="btn btn-primary" onclick="saveRoutingConfig()">💾 保存</button>
729
- </div>
730
- </div>
731
- <div class="panel-body">
732
- <div style="margin-bottom:0.5rem;font-size:0.875rem;color:var(--text-dim)">
733
- <span id="routing-config-source"></span>
734
- <span id="routing-config-path" style="font-family:monospace;margin-left:0.5rem"></span>
735
- </div>
736
- <div id="routing-config-editor" style="width:100%;height:500px;border:1px solid var(--border);border-radius:var(--radius-sm)"></div>
737
- <div id="routing-config-error" style="margin-top:0.5rem;padding:0.75rem;background:var(--danger,#dc2626);color:white;border-radius:var(--radius-sm);display:none"></div>
738
- <div id="routing-config-success" style="margin-top:0.5rem;padding:0.75rem;background:var(--primary);color:white;border-radius:var(--radius-sm);display:none"></div>
739
- </div>
740
- </div>
741
- </div>
742
-
743
- <!-- Experiments subpanel (Phase 5 Feature 1) -->
744
- <div id="routing-sub-experiments" style="display:none">
745
- <div class="panel" style="margin-bottom:1rem">
746
- <div class="panel-header">
747
- <span class="panel-title">实验配置</span>
748
- <div style="display:flex;gap:0.5rem">
749
- <button class="btn" onclick="loadExperimentsConfig()">↻ 重新加载</button>
750
- <button class="btn btn-primary" onclick="saveExperimentsConfig()">💾 保存</button>
751
- </div>
752
- </div>
753
- <div class="panel-body">
754
- <div style="margin-bottom:0.5rem;font-size:0.875rem;color:var(--text-dim)">
755
- <span id="experiments-config-source"></span>
756
- <span id="experiments-config-path" style="font-family:monospace;margin-left:0.5rem"></span>
757
- </div>
758
- <div id="experiments-config-editor" style="width:100%;height:400px;border:1px solid var(--border);border-radius:var(--radius-sm)"></div>
759
- <div id="experiments-config-error" style="margin-top:0.5rem;padding:0.75rem;background:var(--danger,#dc2626);color:white;border-radius:var(--radius-sm);display:none"></div>
760
- <div id="experiments-config-success" style="margin-top:0.5rem;padding:0.75rem;background:var(--primary);color:white;border-radius:var(--radius-sm);display:none"></div>
761
- </div>
762
- </div>
763
- <div class="panel">
764
- <div class="panel-header">
765
- <span class="panel-title">分析对比</span>
766
- <div style="display:flex;gap:0.5rem">
767
- <button class="btn" onclick="loadExperimentsAnalysis()">↻ 刷新分析</button>
768
- </div>
769
- </div>
770
- <div class="panel-body">
771
- <div id="experiments-summary" style="margin-bottom:1rem;font-size:0.875rem"></div>
772
- <div id="experiments-analysis"></div>
773
- <div id="experiments-winner" style="margin-top:1rem;display:none"></div>
774
- </div>
775
- </div>
776
- </div>
777
-
778
- <!-- Recommendations subpanel (Phase 5 Feature 3) -->
779
- <div id="routing-sub-recommendations" style="display:none">
780
- <div class="panel">
781
- <div class="panel-header">
782
- <span class="panel-title">规则推荐</span>
783
- <div style="display:flex;gap:0.5rem;align-items:center">
784
- <label style="font-size:0.875rem;color:var(--text-dim)">窗口
785
- <select class="btn" id="recommendations-days" onchange="loadRecommendations()">
786
- <option value="1">1 天</option>
787
- <option value="7" selected>7 天</option>
788
- <option value="30">30 天</option>
789
- </select>
790
- </label>
791
- <button class="btn" onclick="loadRecommendations()">↻ 刷新</button>
792
- </div>
793
- </div>
794
- <div class="panel-body">
795
- <div style="margin-bottom:0.75rem;font-size:0.875rem;color:var(--text-dim)">
796
- 基于历史决策分析,对比"当前路由规则"与"Claude 实际使用的 agent",
797
- 推荐更贴近实际行为的路由。<br>
798
- 样本阈值:单类任务 ≥ 10 条;挑战者使用率 &gt; 50%。
799
- </div>
800
- <div id="recommendations-list"></div>
801
- </div>
802
- </div>
803
- </div>
804
- </div>
805
-
806
- </div><!-- /container -->
807
- </main>
808
-
809
- <!-- Drawer overlay -->
810
- <div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
811
-
812
- <!-- Drawer -->
813
- <div class="drawer" id="drawer">
814
- <div class="drawer-header">
815
- <div class="drawer-title" id="drawer-title">详情</div>
816
- <button class="drawer-close" onclick="closeDrawer()">
817
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
818
- </button>
819
- </div>
820
- <div class="drawer-body" id="drawer-body"></div>
821
- </div>
822
-
823
- </div><!-- /app -->
824
-
825
- <script>
826
- const API = '';
827
- let allEvents = [], allSessions = [], allInjections = [];
828
- let charts = {};
829
- let traceStream = null;
830
- let traceRefreshTimer = null;
831
- let currentPatchPreview = null;
832
- let currentAIOptimizationData = null;
833
-
834
- // === Navigation ===
835
- function nav(page) {
836
- document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
837
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
838
- const navEl = document.getElementById('nav-' + page);
839
- const pageEl = document.getElementById('page-' + page);
840
- if (navEl) navEl.classList.add('active');
841
- if (pageEl) pageEl.classList.add('active');
842
- const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', 'execution-trace':'执行追踪', agents:'Agent 管理', skills:'Skill 管理', 'ai-config':'AI 配置', routing:'Agent 路由' };
843
- document.getElementById('topbar-title').textContent = titles[page] || page;
844
- closeDrawer();
845
- if (page === 'dashboard') loadDashboard();
846
- else if (page === 'sessions') loadSessions();
847
- else if (page === 'events') loadEvents();
848
- else if (page === 'injections') loadInjections();
849
- else if (page === 'execution-trace') loadExecutionTraces();
850
- else if (page === 'agents') loadAgents();
851
- else if (page === 'skills') loadSkills();
852
- else if (page === 'ai-config') loadAIConfig();
853
- else if (page === 'routing') loadRouting();
854
- }
855
-
856
- function refreshPage() {
857
- const active = document.querySelector('.page.active');
858
- if (active) nav(active.id.replace('page-', ''));
859
- }
860
-
861
- // === Drawer ===
862
- function openDrawer(title, html) {
863
- document.getElementById('drawer-title').textContent = title;
864
- document.getElementById('drawer-body').innerHTML = html;
865
- document.getElementById('drawer').classList.add('active');
866
- document.getElementById('drawer-overlay').classList.add('active');
867
- }
868
- function closeDrawer() {
869
- document.getElementById('drawer').classList.remove('active');
870
- document.getElementById('drawer-overlay').classList.remove('active');
871
- }
872
-
873
- // === Helpers ===
874
- function fmt(ts) {
875
- if (!ts) return '—';
876
- const d = new Date(String(ts).endsWith('Z') ? ts : ts + 'Z');
877
- return d.toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
878
- }
879
- function fmtTime(ts) {
880
- if (!ts) return '—';
881
- const d = new Date(String(ts).endsWith('Z') ? ts : ts + 'Z');
882
- return d.toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
883
- }
884
- function badgeHook(type) {
885
- const m = { PreToolUse:'badge-info', PostToolUse:'badge-allow', UserPromptSubmit:'badge-warn', Stop:'badge-warn' };
886
- return `<span class="badge ${m[type]||'badge-info'}">${type||'—'}</span>`;
887
- }
888
- function empty(msg) {
889
- return `<div class="empty"><div class="empty-icon">📭</div><div class="empty-text">${msg}</div></div>`;
890
- }
891
- function loading() {
892
- return `<div class="loading">加载中...</div>`;
893
- }
894
-
895
- // === Dashboard ===
896
- async function loadDashboard() {
897
- document.getElementById('dash-stats').innerHTML = loading();
898
- try {
899
- const [statusRes, statsRes] = await Promise.all([
900
- fetch(API + '/api/status'),
901
- fetch(API + '/api/stats')
902
- ]);
903
- const status = await statusRes.json();
904
- const stats = await statsRes.json();
905
-
906
- // Stat cards
907
- const uptime = Math.floor(status.uptime);
908
- const uptimeStr = uptime < 60 ? uptime + ' 秒' : Math.floor(uptime/60) + ' 分钟';
909
- const mem = Math.round(status.memory.heapUsed / 1024 / 1024);
910
- document.getElementById('dash-stats').innerHTML = `
911
- <div class="stat-card"><div class="label">进程 ID</div><div class="value">${status.pid}</div></div>
912
- <div class="stat-card"><div class="label">运行时长</div><div class="value">${uptimeStr}</div></div>
913
- <div class="stat-card"><div class="label">内存占用</div><div class="value">${mem}MB</div></div>
914
- <div class="stat-card"><div class="label">事件总数</div><div class="value">${status.eventCount}</div></div>
915
- `;
916
- document.getElementById('daemon-status').textContent = '运行中';
917
- document.getElementById('daemon-status').className = 'badge badge-allow';
918
-
919
- // Charts
920
- renderActivityChart(stats.dailyActivity || []);
921
- renderToolChart(stats.toolUsage || {});
922
- } catch {
923
- document.getElementById('dash-stats').innerHTML = `<div class="stat-card"><div class="label">状态</div><div class="value" style="font-size:1rem;color:var(--red)">离线</div></div>`;
924
- document.getElementById('daemon-status').textContent = '离线';
925
- document.getElementById('daemon-status').className = 'badge badge-block';
926
- }
927
-
928
- // Recent sessions
929
- try {
930
- const res = await fetch(API + '/api/sessions?limit=5');
931
- const sessions = await res.json();
932
- document.getElementById('dash-sessions').innerHTML = sessions.length === 0
933
- ? empty('暂无会话')
934
- : sessions.map(s => sessionListItem(s)).join('');
935
- } catch {
936
- document.getElementById('dash-sessions').innerHTML = empty('加载失败');
937
- }
938
-
939
- // Recent activity
940
- try {
941
- const res = await fetch(API + '/api/events?limit=10');
942
- const events = await res.json();
943
- document.getElementById('dash-activity').innerHTML = events.length === 0
944
- ? empty('暂无活动')
945
- : events.map(e => `
946
- <div class="activity-item">
947
- <div class="activity-dot activity-dot-${hookColor(e.hook_type)}"></div>
948
- <div class="activity-content">
949
- <div class="activity-title">${e.hook_type} ${e.tool_name ? '· ' + e.tool_name : ''}</div>
950
- <div class="activity-time">${fmtTime(e.timestamp)}</div>
951
- </div>
952
- </div>`).join('');
953
- } catch {}
954
- }
955
-
956
- function hookColor(t) {
957
- return { PreToolUse:'blue', PostToolUse:'green', UserPromptSubmit:'yellow', Stop:'red' }[t] || 'blue';
958
- }
959
-
960
- function renderActivityChart(data) {
961
- const ctx = document.getElementById('chart-activity');
962
- if (!ctx) return;
963
- if (charts.activity) charts.activity.destroy();
964
- charts.activity = new Chart(ctx, {
965
- type: 'line',
966
- data: {
967
- labels: data.map(d => d.date),
968
- datasets: [{ label: '事件数', data: data.map(d => d.count), borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.08)', tension: 0.3, fill: true }]
969
- },
970
- options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } }, x: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } } } }
971
- });
972
- }
973
-
974
- function renderToolChart(data) {
975
- const ctx = document.getElementById('chart-tools');
976
- if (!ctx) return;
977
- if (charts.tools) charts.tools.destroy();
978
- const labels = Object.keys(data).slice(0, 8);
979
- const values = labels.map(k => data[k]);
980
- charts.tools = new Chart(ctx, {
981
- type: 'doughnut',
982
- data: { labels, datasets: [{ data: values, backgroundColor: ['#4f46e5','#16a34a','#d97706','#dc2626','#2563eb','#7c3aed','#0891b2','#db2777'] }] },
983
- options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#4b5563', boxWidth: 12, font: { size: 11 } } } } }
984
- });
985
- }
986
-
987
- // === Sessions ===
988
- function sessionListItem(s) {
989
- const prompt = (s.first_prompt || '(无提示词)').slice(0, 60);
990
- return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
991
- <div style="flex:1;min-width:0">
992
- <div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
993
- <div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} 个事件 · ${s.session_id.slice(0,8)}</div>
994
- </div>
995
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
996
- </div>`;
997
- }
998
-
999
- async function loadSessions() {
1000
- document.getElementById('sessions-list').innerHTML = loading();
1001
- try {
1002
- const res = await fetch(API + '/api/sessions?limit=50');
1003
- allSessions = await res.json();
1004
- renderSessions(allSessions);
1005
- } catch {
1006
- document.getElementById('sessions-list').innerHTML = empty('加载会话失败');
1007
- }
1008
- }
1009
-
1010
- function renderSessions(list) {
1011
- document.getElementById('sessions-list').innerHTML = list.length === 0
1012
- ? empty('未找到会话')
1013
- : list.map(s => sessionListItem(s)).join('');
1014
- }
1015
-
1016
- function filterSessions() {
1017
- const q = document.getElementById('sessions-search').value.toLowerCase();
1018
- renderSessions(allSessions.filter(s =>
1019
- (s.first_prompt || '').toLowerCase().includes(q) ||
1020
- s.session_id.toLowerCase().includes(q)
1021
- ));
1022
- }
1023
-
1024
- let currentSessionData = null;
1025
-
1026
- async function openSessionDrawer(sessionId) {
1027
- openDrawer('会话详情', loading());
1028
- try {
1029
- const res = await fetch(API + '/api/sessions/' + sessionId + '/detail');
1030
- const data = await res.json();
1031
- currentSessionData = data;
1032
- const s = data.session;
1033
- let html = '<div class="detail-section"><div class="detail-label">会话 ID</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + s.session_id + '</div></div>';
1034
- html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(s.start_time) + ' → ' + fmt(s.end_time) + '</div></div>';
1035
- html += '<div class="detail-section"><div class="detail-label">首条提示词</div><div class="detail-value">' + (s.first_prompt || '—') + '</div></div>';
1036
- html += '<div class="detail-section"><div class="detail-label">任务(' + data.tasks.length + ')</div>';
1037
- data.tasks.forEach((t, i) => {
1038
- const tools = t.events.filter(e => e.tool_name).length;
1039
- html += '<div onclick="openTaskDrawer(' + i + ')" style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border);cursor:pointer;transition:all 0.15s" onmouseover="this.style.borderColor=\'var(--primary)\';this.style.background=\'var(--primary-soft)\'" onmouseout="this.style.borderColor=\'var(--border)\';this.style.background=\'var(--bg-secondary)\'">';
1040
- html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px"><div style="font-weight:500;font-size:0.875rem;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:0.5rem">任务 ' + (i+1) + ':' + t.title + '</div><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>';
1041
- html += '<div style="font-size:0.75rem;color:var(--text-dim)">' + fmtTime(t.start_time) + ' · ' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件</div>';
1042
- if (t.summary.commits.length > 0) html += '<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ' + t.summary.commits.length + ' 次提交</div>';
1043
- html += '</div>';
1044
- });
1045
- html += '</div>';
1046
- openDrawer('会话 · ' + s.session_id.slice(0,8), html);
1047
- } catch(e) {
1048
- openDrawer('会话详情', empty('加载失败:' + e.message));
1049
- }
1050
- }
1051
-
1052
- function openTaskDrawer(taskIndex) {
1053
- if (!currentSessionData || !currentSessionData.tasks[taskIndex]) return;
1054
- const t = currentSessionData.tasks[taskIndex];
1055
- const tools = t.events.filter(e => e.tool_name).length;
1056
-
1057
- let html = '<div class="detail-section"><div class="detail-label">标题</div><div class="detail-value" style="font-weight:600">' + t.title + '</div></div>';
1058
- html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(t.start_time) + (t.end_time ? ' → ' + fmt(t.end_time) : '') + '</div></div>';
1059
- html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件变更 · ' + t.summary.commits.length + ' 次提交</div></div>';
1060
-
1061
- // User prompts
1062
- if (t.prompts && t.prompts.length > 0) {
1063
- html += '<div class="detail-section"><div class="detail-label">用户提示词(' + t.prompts.length + ')</div>';
1064
- t.prompts.forEach(p => {
1065
- html += '<div style="padding:0.625rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
1066
- html += '<div style="color:var(--text-dim);font-size:0.7rem;margin-bottom:2px">' + fmtTime(p.timestamp) + '</div>';
1067
- html += '<div style="white-space:pre-wrap;word-break:break-word">' + (p.content || '—') + '</div>';
1068
- html += '</div>';
1069
- });
1070
- html += '</div>';
1071
- }
1072
-
1073
- // Tool usage summary
1074
- if (t.summary.toolUsage && Object.keys(t.summary.toolUsage).length > 0) {
1075
- html += '<div class="detail-section"><div class="detail-label">工具使用分布</div><div class="detail-value">';
1076
- Object.entries(t.summary.toolUsage).sort((a,b) => b[1]-a[1]).forEach(([name, count]) => {
1077
- html += '<span class="tag" style="margin-bottom:4px">' + name + ' × ' + count + '</span>';
1078
- });
1079
- html += '</div></div>';
1080
- }
1081
-
1082
- // Files changed
1083
- if (t.summary.filesChanged && t.summary.filesChanged.length > 0) {
1084
- html += '<div class="detail-section"><div class="detail-label">变更的文件(' + t.summary.filesChanged.length + ')</div>';
1085
- t.summary.filesChanged.forEach(f => {
1086
- const short = f.split('/').slice(-2).join('/');
1087
- html += '<div style="font-family:monospace;font-size:0.75rem;color:var(--text-muted);padding:2px 0" title="' + f + '">' + short + '</div>';
1088
- });
1089
- html += '</div>';
1090
- }
1091
-
1092
- // Commits
1093
- if (t.summary.commits && t.summary.commits.length > 0) {
1094
- html += '<div class="detail-section"><div class="detail-label">提交记录(' + t.summary.commits.length + ')</div>';
1095
- t.summary.commits.forEach(c => {
1096
- html += '<div style="padding:0.5rem;background:var(--green-soft);border-radius:var(--radius-sm);margin-bottom:0.25rem;font-size:0.8rem;color:var(--text)"><span style="color:var(--green)">✓</span> ' + c.message + '</div>';
1097
- });
1098
- html += '</div>';
1099
- }
1100
-
1101
- // Injections
1102
- if (t.injections && t.injections.length > 0) {
1103
- html += '<div class="detail-section"><div class="detail-label">注入内容(' + t.injections.length + ')</div>';
1104
- t.injections.forEach(inj => {
1105
- const escaped = inj.content.replace(/</g, '&lt;').replace(/>/g, '&gt;');
1106
- html += '<div style="padding:0.625rem;background:var(--primary-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--primary-border)">';
1107
- html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:4px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.7rem;color:var(--text-dim)">' + inj.source_handler + ' · ' + fmtTime(inj.timestamp) + '</span></div>';
1108
- html += '<div style="font-size:0.75rem;color:var(--text);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto;font-family:ui-monospace,monospace">' + escaped + '</div>';
1109
- html += '</div>';
1110
- });
1111
- html += '</div>';
1112
- }
1113
-
1114
- // Quality issues
1115
- if (t.qualityIssues && t.qualityIssues.length > 0) {
1116
- html += '<div class="detail-section"><div class="detail-label">质量问题(' + t.qualityIssues.length + ')</div>';
1117
- t.qualityIssues.forEach(q => {
1118
- const sev = q.severity || 'info';
1119
- const badgeClass = sev === 'must' ? 'badge-block' : sev === 'should' ? 'badge-warn' : 'badge-info';
1120
- const sevLabel = sev === 'must' ? '必须' : sev === 'should' ? '建议' : '提示';
1121
- html += '<div style="padding:0.5rem;background:var(--yellow-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
1122
- html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + sevLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (q.file_path || '').split('/').slice(-2).join('/') + '</span></div>';
1123
- html += '<div style="color:var(--text)">' + (q.description || q.message || '') + '</div>';
1124
- html += '</div>';
1125
- });
1126
- html += '</div>';
1127
- }
1128
-
1129
- // Governance decisions
1130
- if (t.decisions && t.decisions.length > 0) {
1131
- html += '<div class="detail-section"><div class="detail-label">治理决策(' + t.decisions.length + ')</div>';
1132
- t.decisions.forEach(d => {
1133
- const lv = d.level || 'info';
1134
- const badgeClass = lv === 'block' ? 'badge-block' : lv === 'warn' ? 'badge-warn' : lv === 'allow' ? 'badge-allow' : 'badge-info';
1135
- const lvLabel = lv === 'block' ? '阻断' : lv === 'warn' ? '警告' : lv === 'allow' ? '放行' : lv === 'confirm' ? '确认' : lv;
1136
- html += '<div style="padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
1137
- html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + lvLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (d.rule_id || '') + '</span></div>';
1138
- html += '<div style="color:var(--text-muted)">' + (d.reason || '—') + '</div>';
1139
- html += '</div>';
1140
- });
1141
- html += '</div>';
1142
- }
1143
-
1144
- // Recent events
1145
- if (t.events && t.events.length > 0) {
1146
- html += '<div class="detail-section"><div class="detail-label">事件(展示 ' + Math.min(t.events.length, 30) + ' / ' + t.events.length + ')</div>';
1147
- t.events.slice(0, 30).forEach(e => {
1148
- const inputPreview = e.tool_input ? (JSON.stringify(e.tool_input).slice(0, 80) + (JSON.stringify(e.tool_input).length > 80 ? '...' : '')) : '';
1149
- html += '<div style="padding:0.5rem;border-bottom:1px solid var(--border);font-size:0.75rem">';
1150
- html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px">';
1151
- html += '<span style="color:var(--text-dim)">' + fmtTime(e.timestamp) + '</span>';
1152
- html += badgeHook(e.hook_type);
1153
- if (e.tool_name) html += '<span style="font-family:monospace;color:var(--text-muted)">' + e.tool_name + '</span>';
1154
- html += '</div>';
1155
- if (inputPreview) html += '<div style="font-family:monospace;color:var(--text-dim);font-size:0.7rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + inputPreview + '</div>';
1156
- html += '</div>';
1157
- });
1158
- html += '</div>';
1159
- }
1160
-
1161
- // Back button
1162
- html = '<div style="margin-bottom:1rem"><button class="btn" onclick="openSessionDrawer(\'' + currentSessionData.session.session_id + '\')">← 返回会话</button></div>' + html;
1163
-
1164
- openDrawer('任务:' + t.title.slice(0, 40), html);
1165
- }
1166
-
1167
- // === Events ===
1168
- async function loadEvents() {
1169
- document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading">加载中...</td></tr>';
1170
- try {
1171
- const res = await fetch(API + '/api/events?limit=100');
1172
- allEvents = await res.json();
1173
- renderEvents(allEvents);
1174
- } catch {
1175
- document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('加载失败') + '</td></tr>';
1176
- }
1177
- }
1178
-
1179
- function renderEvents(list) {
1180
- document.getElementById('events-body').innerHTML = list.length === 0
1181
- ? '<tr><td colspan="4">' + empty('暂无事件') + '</td></tr>'
1182
- : list.map(e => '<tr onclick="openEventDrawer(' + JSON.stringify(e).replace(/"/g,'&quot;') + ')"><td style="color:var(--text-dim);font-size:0.8rem">' + fmtTime(e.timestamp) + '</td><td>' + badgeHook(e.hook_type) + '</td><td style="font-family:monospace;font-size:0.8rem">' + (e.tool_name || '—') + '</td><td style="color:var(--text-dim);font-size:0.8rem">' + ((e.project_path||'').split('/').pop() || '—') + '</td></tr>').join('');
1183
- }
1184
-
1185
- function filterEvents() {
1186
- const q = document.getElementById('events-search').value.toLowerCase();
1187
- const t = document.getElementById('events-type').value;
1188
- renderEvents(allEvents.filter(e =>
1189
- (!t || e.hook_type === t) &&
1190
- (!q || (e.hook_type||'').toLowerCase().includes(q) || (e.tool_name||'').toLowerCase().includes(q) || (e.project_path||'').toLowerCase().includes(q))
1191
- ));
1192
- }
1193
-
1194
- function openEventDrawer(e) {
1195
- let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value">' + badgeHook(e.hook_type) + '</div></div>';
1196
- html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(e.timestamp) + '</div></div>';
1197
- html += '<div class="detail-section"><div class="detail-label">工具</div><div class="detail-value" style="font-family:monospace">' + (e.tool_name || '—') + '</div></div>';
1198
- html += '<div class="detail-section"><div class="detail-label">项目</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.project_path || '—') + '</div></div>';
1199
- html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.session_id || '—') + '</div></div>';
1200
- if (e.tool_input) html += '<div class="detail-section"><div class="detail-label">输入参数</div><pre class="detail-code">' + JSON.stringify(e.tool_input, null, 2) + '</pre></div>';
1201
- openDrawer(e.hook_type + ' · ' + (e.tool_name || '事件'), html);
1202
- }
1203
-
1204
- // === Injections ===
1205
- async function loadInjections() {
1206
- document.getElementById('inj-list').innerHTML = loading();
1207
- try {
1208
- const res = await fetch(API + '/api/injections?limit=100');
1209
- allInjections = await res.json();
1210
- renderInjections(allInjections);
1211
- } catch {
1212
- document.getElementById('inj-list').innerHTML = empty('加载失败');
1213
- }
1214
- }
1215
-
1216
- function renderInjections(list) {
1217
- document.getElementById('inj-list').innerHTML = list.length === 0 ? empty('暂无注入记录') : list.map(inj => '<div class="list-item fade-in" onclick="openInjDrawer(' + JSON.stringify(inj).replace(/"/g,'&quot;') + ')"><div style="flex:1;min-width:0"><div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:2px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.75rem;color:var(--text-dim)">' + inj.source_handler + '</span></div><div style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + inj.content.slice(0,80) + '</div><div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">' + fmt(inj.timestamp) + ' · ' + inj.session_id.slice(0,8) + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
1218
- }
1219
-
1220
- function filterInjections() {
1221
- const q = document.getElementById('inj-search').value.toLowerCase();
1222
- const h = document.getElementById('inj-handler').value;
1223
- renderInjections(allInjections.filter(inj => (!h || inj.source_handler === h) && (!q || inj.content.toLowerCase().includes(q) || inj.injection_type.toLowerCase().includes(q))));
1224
- }
1225
-
1226
- function openInjDrawer(inj) {
1227
- let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value"><span class="badge badge-info">' + inj.injection_type + '</span></div></div>';
1228
- html += '<div class="detail-section"><div class="detail-label">Handler</div><div class="detail-value">' + inj.source_handler + '</div></div>';
1229
- html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(inj.timestamp) + '</div></div>';
1230
- html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + inj.session_id + '</div></div>';
1231
- html += '<div class="detail-section"><div class="detail-label">注入内容</div><pre class="detail-code">' + inj.content + '</pre></div>';
1232
- openDrawer(inj.injection_type, html);
1233
- }
1234
-
1235
- // === Rules ===
1236
-
1237
- // === Agent Routing ===
1238
- let routingCurrentTab = 'overview';
1239
-
1240
- function routingTab(tab) {
1241
- routingCurrentTab = tab;
1242
- ['overview', 'events', 'refusals', 'performance', 'ai-optimization', 'editor', 'experiments', 'recommendations'].forEach(t => {
1243
- const btn = document.getElementById('routing-tab-' + t);
1244
- const sub = document.getElementById('routing-sub-' + t);
1245
- if (btn) btn.classList.toggle('active', t === tab);
1246
- if (sub) sub.style.display = (t === tab) ? '' : 'none';
1247
- });
1248
-
1249
- // Show/hide time window selector (not needed for editor/experiments/recommendations tabs)
1250
- const windowSelector = document.getElementById('routing-window');
1251
- if (windowSelector) {
1252
- windowSelector.style.display =
1253
- (tab === 'editor' || tab === 'experiments' || tab === 'recommendations') ? 'none' : '';
1254
- }
1255
-
1256
- loadRouting();
1257
- }
1258
-
1259
- async function loadRouting() {
1260
- if (routingCurrentTab === 'overview') return loadRoutingOverview();
1261
- if (routingCurrentTab === 'events') return loadRoutingEvents();
1262
- if (routingCurrentTab === 'refusals') return loadRoutingRefusals();
1263
- if (routingCurrentTab === 'performance') return loadRoutingPerformance();
1264
- if (routingCurrentTab === 'ai-optimization') return loadRoutingAIOptimization();
1265
- if (routingCurrentTab === 'editor') return loadRoutingConfig();
1266
- if (routingCurrentTab === 'experiments') {
1267
- await loadExperimentsConfig();
1268
- return loadExperimentsAnalysis();
1269
- }
1270
- if (routingCurrentTab === 'recommendations') return loadRecommendations();
1271
- }
1272
-
1273
- // === AI Config ===
1274
- let aiApiKeyMasked = true;
1275
- let aiApiKeyOriginal = '';
1276
-
1277
- async function loadAIConfig() {
1278
- try {
1279
- const r = await fetch(API + '/api/config/ai');
1280
- if (!r.ok) throw new Error('Failed to load config');
1281
- const data = await r.json();
1282
-
1283
- document.getElementById('ai-api-key').value = data.api_key || '';
1284
- document.getElementById('ai-base-url').value = data.base_url || '';
1285
- document.getElementById('ai-provider').value = data.provider || 'anthropic';
1286
- document.getElementById('ai-classifier-timeout').value = data.classifier_timeout || 10000;
1287
- aiApiKeyOriginal = data.api_key || '';
1288
- aiApiKeyMasked = true;
1289
- document.getElementById('ai-api-key').type = 'password';
1290
- document.getElementById('toggle-api-key-btn').textContent = '👁️ 显示';
1291
-
1292
- // Pre-fill model dropdown with current model
1293
- const modelSelect = document.getElementById('ai-model');
1294
- modelSelect.innerHTML = `<option value="${data.model}">${data.model}</option>`;
1295
-
1296
- // Pre-fill classifier model dropdown
1297
- const classifierSelect = document.getElementById('ai-classifier-model');
1298
- const classifierModel = data.classifier_model || '';
1299
- classifierSelect.innerHTML = classifierModel
1300
- ? `<option value="">(与主模型相同)</option><option value="${classifierModel}" selected>${classifierModel}</option>`
1301
- : `<option value="" selected>(与主模型相同)</option>`;
1302
- } catch (err) {
1303
- showAIError(`加载配置失败: ${err.message}`);
1304
- }
1305
- }
1306
-
1307
- function toggleAPIKeyVisibility() {
1308
- const input = document.getElementById('ai-api-key');
1309
- const btn = document.getElementById('toggle-api-key-btn');
1310
- if (input.type === 'password') {
1311
- input.type = 'text';
1312
- btn.textContent = '🙈 隐藏';
1313
- } else {
1314
- input.type = 'password';
1315
- btn.textContent = '👁️ 显示';
1316
- }
1317
- }
1318
-
1319
- async function refreshAIModels() {
1320
- const modelSelect = document.getElementById('ai-model');
1321
- const classifierSelect = document.getElementById('ai-classifier-model');
1322
- const currentValue = modelSelect.value;
1323
- const currentClassifier = classifierSelect.value;
1324
- modelSelect.innerHTML = '<option value="">加载中...</option>';
1325
- try {
1326
- // Pass current form values so user can refresh before saving
1327
- const apiKeyInput = document.getElementById('ai-api-key').value;
1328
- const baseUrl = document.getElementById('ai-base-url').value;
1329
- const params = new URLSearchParams();
1330
- // Only send api_key if user typed a fresh one (not the masked placeholder)
1331
- if (apiKeyInput && !apiKeyInput.includes('***')) params.set('api_key', apiKeyInput);
1332
- if (baseUrl) params.set('base_url', baseUrl);
1333
- const qs = params.toString() ? '?' + params.toString() : '';
1334
-
1335
- const r = await fetch(API + '/api/ai/models' + qs);
1336
- if (!r.ok) {
1337
- const err = await r.json().catch(() => ({ error: r.statusText }));
1338
- throw new Error(err.error || r.statusText);
1339
- }
1340
- const data = await r.json();
1341
- const models = data.data || [];
1342
- if (models.length === 0) {
1343
- modelSelect.innerHTML = '<option value="">无可用模型</option>';
1344
- showAIError('未找到可用模型');
1345
- return;
1346
- }
1347
- modelSelect.innerHTML = models.map(m => {
1348
- const selected = m.id === currentValue ? 'selected' : '';
1349
- return `<option value="${m.id}" ${selected}>${m.id}</option>`;
1350
- }).join('');
1351
-
1352
- // Also populate classifier model dropdown
1353
- classifierSelect.innerHTML = '<option value="">(与主模型相同)</option>' + models.map(m => {
1354
- const selected = m.id === currentClassifier ? 'selected' : '';
1355
- return `<option value="${m.id}" ${selected}>${m.id}</option>`;
1356
- }).join('');
1357
-
1358
- showAISuccess(`已加载 ${models.length} 个模型`);
1359
- } catch (err) {
1360
- modelSelect.innerHTML = `<option value="${currentValue}">${currentValue}</option>`;
1361
- showAIError(`加载模型列表失败: ${err.message}`);
1362
- }
1363
- }
1364
-
1365
- async function saveAIConfig() {
1366
- const apiKeyInput = document.getElementById('ai-api-key').value;
1367
- const baseUrl = document.getElementById('ai-base-url').value;
1368
- const provider = document.getElementById('ai-provider').value;
1369
- const model = document.getElementById('ai-model').value;
1370
- const classifierModel = document.getElementById('ai-classifier-model').value;
1371
- const classifierTimeout = parseInt(document.getElementById('ai-classifier-timeout').value) || undefined;
1372
-
1373
- const body = { base_url: baseUrl, provider, model };
1374
- if (classifierModel) body.classifier_model = classifierModel;
1375
- if (classifierTimeout) body.classifier_timeout = classifierTimeout;
1376
-
1377
- // Only send api_key if user changed it (not the masked placeholder)
1378
- if (apiKeyInput && apiKeyInput !== aiApiKeyOriginal && !apiKeyInput.includes('***')) {
1379
- body.api_key = apiKeyInput;
1380
- }
1381
-
1382
- try {
1383
- const r = await fetch(API + '/api/config/ai', {
1384
- method: 'PUT',
1385
- headers: { 'Content-Type': 'application/json' },
1386
- body: JSON.stringify(body),
1387
- });
1388
- if (!r.ok) {
1389
- const err = await r.json().catch(() => ({ error: r.statusText }));
1390
- throw new Error(err.error || r.statusText);
1391
- }
1392
- showAISuccess('✓ 保存成功!重启 daemon 后生效');
1393
- loadAIConfig();
1394
- } catch (err) {
1395
- showAIError(`保存失败: ${err.message}`);
1396
- }
1397
- }
1398
-
1399
- async function testAIConnection() {
1400
- showAISuccess('正在测试连接...');
1401
- try {
1402
- const apiKeyInput = document.getElementById('ai-api-key').value;
1403
- const baseUrl = document.getElementById('ai-base-url').value;
1404
- const model = document.getElementById('ai-model').value;
1405
- const body = {};
1406
- if (apiKeyInput && !apiKeyInput.includes('***')) body.api_key = apiKeyInput;
1407
- if (baseUrl) body.base_url = baseUrl;
1408
- if (model) body.model = model;
1409
-
1410
- const r = await fetch(API + '/api/ai/test', {
1411
- method: 'POST',
1412
- headers: { 'Content-Type': 'application/json' },
1413
- body: JSON.stringify(body),
1414
- });
1415
- if (!r.ok) {
1416
- const err = await r.json().catch(() => ({ error: r.statusText }));
1417
- throw new Error(err.error || r.statusText);
1418
- }
1419
- const data = await r.json();
1420
- showAISuccess(`✓ 连接成功!模型: ${data.model || model}`);
1421
- } catch (err) {
1422
- showAIError(`连接失败: ${err.message}`);
1423
- }
1424
- }
1425
-
1426
- function showAISuccess(msg) {
1427
- const el = document.getElementById('ai-config-success');
1428
- const errEl = document.getElementById('ai-config-error');
1429
- el.textContent = msg;
1430
- el.style.display = 'block';
1431
- errEl.style.display = 'none';
1432
- setTimeout(() => { el.style.display = 'none'; }, 5000);
1433
- }
1434
-
1435
- function showAIError(msg) {
1436
- const el = document.getElementById('ai-config-error');
1437
- const okEl = document.getElementById('ai-config-success');
1438
- el.textContent = msg;
1439
- el.style.display = 'block';
1440
- okEl.style.display = 'none';
1441
- }
1442
-
1443
- // === Agent Management ===
1444
- async function loadAgents() {
1445
- try {
1446
- const r = await fetch(API + '/api/agents');
1447
- if (!r.ok) throw new Error('Failed to load agents');
1448
- const data = await r.json();
1449
-
1450
- const tbody = document.getElementById('agents-body');
1451
- const rows = [];
1452
-
1453
- data.official.forEach(a => {
1454
- rows.push(`<tr>
1455
- <td><strong>${a.name}</strong></td>
1456
- <td>${a.description}</td>
1457
- <td><span class="badge badge-info">${a.category}</span></td>
1458
- <td>${a.version}</td>
1459
- <td><span class="badge badge-allow">官方</span></td>
1460
- <td>
1461
- <button class="btn" onclick="viewAgent('${a.name}')">查看</button>
1462
- <button class="btn" onclick="editAgent('${a.name}')">编辑</button>
1463
- </td>
1464
- </tr>`);
1465
- });
1466
-
1467
- data.user.forEach(a => {
1468
- rows.push(`<tr>
1469
- <td><strong>${a.name}</strong></td>
1470
- <td>${a.description}</td>
1471
- <td>—</td>
1472
- <td>${a.version || '—'}</td>
1473
- <td><span class="badge badge-warn">用户</span></td>
1474
- <td>
1475
- <button class="btn" onclick="viewAgent('${a.name}')">查看</button>
1476
- <button class="btn" onclick="editAgent('${a.name}')">编辑</button>
1477
- </td>
1478
- </tr>`);
1479
- });
1480
-
1481
- tbody.innerHTML = rows.length > 0 ? rows.join('') : '<tr><td colspan="6" style="text-align:center;color:var(--text-dim)">暂无 Agent</td></tr>';
1482
- } catch (err) {
1483
- console.error('Failed to load agents:', err);
1484
- }
1485
- }
1486
-
1487
- async function viewAgent(name) {
1488
- try {
1489
- const r = await fetch(API + `/api/agents/${name}`);
1490
- if (!r.ok) throw new Error('Failed to load agent');
1491
- const data = await r.json();
1492
-
1493
- const html = `
1494
- <div style="margin-bottom:1rem">
1495
- <h3>${data.name}</h3>
1496
- <p style="color:var(--text-dim)">${data.description}</p>
1497
- <div style="margin-top:0.5rem">
1498
- <span class="badge badge-info">${data.version || 'v1.0.0'}</span>
1499
- ${data.tools ? `<span class="badge badge-allow">${data.tools}</span>` : ''}
1500
- </div>
1501
- </div>
1502
- <div style="margin-bottom:1rem;display:flex;gap:0.5rem">
1503
- <button class="btn" onclick="viewAgentVersions('${name}')">📜 版本历史</button>
1504
- </div>
1505
- <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1506
- <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1507
- </div>
1508
- `;
1509
- openDrawer(`Agent: ${name}`, html);
1510
- } catch (err) {
1511
- alert('加载失败: ' + err.message);
1512
- }
1513
- }
1514
-
1515
- async function editAgent(name) {
1516
- try {
1517
- const r = await fetch(API + `/api/agents/${name}`);
1518
- if (!r.ok) throw new Error('Failed to load agent');
1519
- const data = await r.json();
1520
-
1521
- const html = `
1522
- <div style="margin-bottom:1rem">
1523
- <h3>编辑 Agent: ${data.name}</h3>
1524
- <p style="color:var(--text-dim)">${data.description}</p>
1525
- </div>
1526
- <textarea id="edit-agent-content" style="width:100%;height:60vh;font-family:monospace;font-size:0.875rem;padding:0.5rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary)">${escapeHtml(data.content)}</textarea>
1527
- <div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end">
1528
- <button class="btn" onclick="closeDrawer()">取消</button>
1529
- <button class="btn btn-primary" onclick="saveAgent('${name}')">💾 保存</button>
1530
- </div>
1531
- <div id="edit-agent-msg" style="margin-top:0.5rem;padding:0.5rem;border-radius:var(--radius-sm);display:none"></div>
1532
- `;
1533
- openDrawer(`编辑 Agent: ${name}`, html);
1534
- } catch (err) {
1535
- alert('加载失败: ' + err.message);
1536
- }
1537
- }
1538
-
1539
- async function saveAgent(name) {
1540
- const content = document.getElementById('edit-agent-content').value;
1541
- const msgEl = document.getElementById('edit-agent-msg');
1542
- try {
1543
- const r = await fetch(API + `/api/agents/${name}`, {
1544
- method: 'PUT',
1545
- headers: { 'Content-Type': 'application/json' },
1546
- body: JSON.stringify({ content }),
1547
- });
1548
- if (!r.ok) throw new Error('Failed to save agent');
1549
- const data = await r.json();
1550
- msgEl.textContent = `✓ 保存成功!备份: ${data.backup}`;
1551
- msgEl.style.background = 'var(--primary)';
1552
- msgEl.style.color = 'white';
1553
- msgEl.style.display = 'block';
1554
- setTimeout(() => { closeDrawer(); loadAgents(); }, 1500);
1555
- } catch (err) {
1556
- msgEl.textContent = `保存失败: ${err.message}`;
1557
- msgEl.style.background = 'var(--red)';
1558
- msgEl.style.color = 'white';
1559
- msgEl.style.display = 'block';
1560
- }
1561
- }
1562
-
1563
- // === Skill Management ===
1564
- async function loadSkills() {
1565
- try {
1566
- const r = await fetch(API + '/api/skills');
1567
- if (!r.ok) throw new Error('Failed to load skills');
1568
- const data = await r.json();
1569
-
1570
- const tbody = document.getElementById('skills-body');
1571
- const rows = data.skills.map(s => `<tr>
1572
- <td><strong>${s.name}</strong></td>
1573
- <td>${s.description}</td>
1574
- <td>${s.version || '—'}</td>
1575
- <td><span class="badge ${s.source === 'official' ? 'badge-allow' : 'badge-warn'}">${s.source === 'official' ? '官方' : '用户'}</span></td>
1576
- <td>
1577
- <button class="btn" onclick="viewSkill('${s.name}')">查看</button>
1578
- <button class="btn" onclick="editSkill('${s.name}')">编辑</button>
1579
- </td>
1580
- </tr>`);
1581
-
1582
- tbody.innerHTML = rows.length > 0 ? rows.join('') : '<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">暂无 Skill</td></tr>';
1583
- } catch (err) {
1584
- console.error('Failed to load skills:', err);
1585
- }
1586
- }
1587
-
1588
- async function viewAgentVersions(name) {
1589
- try {
1590
- const r = await fetch(API + `/api/agents/${name}/versions`);
1591
- if (!r.ok) throw new Error('Failed to load versions');
1592
- const data = await r.json();
1593
-
1594
- if (data.versions.length === 0) {
1595
- alert('暂无版本历史');
1596
- return;
1597
- }
1598
-
1599
- const html = `
1600
- <div style="margin-bottom:1rem">
1601
- <h3>版本历史: ${name}</h3>
1602
- <p style="color:var(--text-dim)">共 ${data.versions.length} 个备份版本</p>
1603
- </div>
1604
- <div style="max-height:60vh;overflow-y:auto">
1605
- ${data.versions.map(v => `
1606
- <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
1607
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
1608
- <div>
1609
- <div style="font-weight:500">${new Date(v.mtime).toLocaleString('zh-CN')}</div>
1610
- <div style="font-size:0.75rem;color:var(--text-dim)">${(v.size / 1024).toFixed(1)} KB</div>
1611
- </div>
1612
- <div style="display:flex;gap:0.25rem">
1613
- <button class="btn" onclick="viewAgentVersion('${name}', '${v.timestamp}')">查看</button>
1614
- <button class="btn btn-primary" onclick="rollbackAgent('${name}', '${v.timestamp}')">回滚</button>
1615
- </div>
1616
- </div>
1617
- </div>
1618
- `).join('')}
1619
- </div>
1620
- `;
1621
- openDrawer(`版本历史: ${name}`, html);
1622
- } catch (err) {
1623
- alert('加载失败: ' + err.message);
1624
- }
1625
- }
1626
-
1627
- async function viewAgentVersion(name, timestamp) {
1628
- try {
1629
- const r = await fetch(API + `/api/agents/${name}/versions/${encodeURIComponent(timestamp)}`);
1630
- if (!r.ok) throw new Error('Failed to load version');
1631
- const data = await r.json();
1632
-
1633
- const html = `
1634
- <div style="margin-bottom:1rem">
1635
- <h3>历史版本: ${name}</h3>
1636
- <p style="color:var(--text-dim)">${new Date(timestamp).toLocaleString('zh-CN')}</p>
1637
- </div>
1638
- <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1639
- <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1640
- </div>
1641
- `;
1642
- openDrawer(`历史版本: ${name}`, html);
1643
- } catch (err) {
1644
- alert('加载失败: ' + err.message);
1645
- }
1646
- }
1647
-
1648
- async function rollbackAgent(name, timestamp) {
1649
- if (!confirm(`确定要回滚到 ${new Date(timestamp).toLocaleString('zh-CN')} 的版本吗?\n\n当前版本会自动备份。`)) {
1650
- return;
1651
- }
1652
-
1653
- try {
1654
- const r = await fetch(API + `/api/agents/${name}/rollback`, {
1655
- method: 'POST',
1656
- headers: { 'Content-Type': 'application/json' },
1657
- body: JSON.stringify({ timestamp }),
1658
- });
1659
- if (!r.ok) throw new Error('Failed to rollback');
1660
- const data = await r.json();
1661
- alert(`✓ 回滚成功!\n\n当前版本已备份到:\n${data.backup}`);
1662
- closeDrawer();
1663
- loadAgents();
1664
- } catch (err) {
1665
- alert('回滚失败: ' + err.message);
1666
- }
1667
- }
1668
-
1669
- async function viewSkill(name) {
1670
- try {
1671
- const r = await fetch(API + `/api/skills/${name}`);
1672
- if (!r.ok) throw new Error('Failed to load skill');
1673
- const data = await r.json();
1674
-
1675
- const html = `
1676
- <div style="margin-bottom:1rem">
1677
- <h3>${data.name}</h3>
1678
- <p style="color:var(--text-dim)">${data.description}</p>
1679
- <div style="margin-top:0.5rem">
1680
- <span class="badge badge-info">${data.version || 'v1.0.0'}</span>
1681
- <span class="badge ${data.source === 'official' ? 'badge-allow' : 'badge-warn'}">${data.source === 'official' ? '官方' : '用户'}</span>
1682
- </div>
1683
- </div>
1684
- <div style="margin-bottom:1rem;display:flex;gap:0.5rem">
1685
- <button class="btn" onclick="viewSkillVersions('${name}')">📜 版本历史</button>
1686
- </div>
1687
- <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1688
- <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1689
- </div>
1690
- `;
1691
- openDrawer(`Skill: ${name}`, html);
1692
- } catch (err) {
1693
- alert('加载失败: ' + err.message);
1694
- }
1695
- }
1696
-
1697
- async function viewSkillVersions(name) {
1698
- try {
1699
- const r = await fetch(API + `/api/skills/${name}/versions`);
1700
- if (!r.ok) throw new Error('Failed to load versions');
1701
- const data = await r.json();
1702
-
1703
- if (data.versions.length === 0) {
1704
- alert('暂无版本历史');
1705
- return;
1706
- }
1707
-
1708
- const html = `
1709
- <div style="margin-bottom:1rem">
1710
- <h3>版本历史: ${name}</h3>
1711
- <p style="color:var(--text-dim)">共 ${data.versions.length} 个备份版本</p>
1712
- </div>
1713
- <div style="max-height:60vh;overflow-y:auto">
1714
- ${data.versions.map(v => `
1715
- <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
1716
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
1717
- <div>
1718
- <div style="font-weight:500">${new Date(v.mtime).toLocaleString('zh-CN')}</div>
1719
- <div style="font-size:0.75rem;color:var(--text-dim)">${(v.size / 1024).toFixed(1)} KB</div>
1720
- </div>
1721
- <div style="display:flex;gap:0.25rem">
1722
- <button class="btn" onclick="viewSkillVersion('${name}', '${v.timestamp}')">查看</button>
1723
- <button class="btn btn-primary" onclick="rollbackSkill('${name}', '${v.timestamp}')">回滚</button>
1724
- </div>
1725
- </div>
1726
- </div>
1727
- `).join('')}
1728
- </div>
1729
- `;
1730
- openDrawer(`版本历史: ${name}`, html);
1731
- } catch (err) {
1732
- alert('加载失败: ' + err.message);
1733
- }
1734
- }
1735
-
1736
- async function viewSkillVersion(name, timestamp) {
1737
- try {
1738
- const r = await fetch(API + `/api/skills/${name}/versions/${encodeURIComponent(timestamp)}`);
1739
- if (!r.ok) throw new Error('Failed to load version');
1740
- const data = await r.json();
1741
-
1742
- const html = `
1743
- <div style="margin-bottom:1rem">
1744
- <h3>历史版本: ${name}</h3>
1745
- <p style="color:var(--text-dim)">${new Date(timestamp).toLocaleString('zh-CN')}</p>
1746
- </div>
1747
- <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1748
- <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1749
- </div>
1750
- `;
1751
- openDrawer(`历史版本: ${name}`, html);
1752
- } catch (err) {
1753
- alert('加载失败: ' + err.message);
1754
- }
1755
- }
1756
-
1757
- async function rollbackSkill(name, timestamp) {
1758
- if (!confirm(`确定要回滚到 ${new Date(timestamp).toLocaleString('zh-CN')} 的版本吗?\n\n当前版本会自动备份。`)) {
1759
- return;
1760
- }
1761
-
1762
- try {
1763
- const r = await fetch(API + `/api/skills/${name}/rollback`, {
1764
- method: 'POST',
1765
- headers: { 'Content-Type': 'application/json' },
1766
- body: JSON.stringify({ timestamp }),
1767
- });
1768
- if (!r.ok) throw new Error('Failed to rollback');
1769
- const data = await r.json();
1770
- alert(`✓ 回滚成功!\n\n当前版本已备份到:\n${data.backup}`);
1771
- closeDrawer();
1772
- loadSkills();
1773
- } catch (err) {
1774
- alert('回滚失败: ' + err.message);
1775
- }
1776
- }
1777
-
1778
- async function editSkill(name) {
1779
- try {
1780
- const r = await fetch(API + `/api/skills/${name}`);
1781
- if (!r.ok) throw new Error('Failed to load skill');
1782
- const data = await r.json();
1783
-
1784
- const html = `
1785
- <div style="margin-bottom:1rem">
1786
- <h3>编辑 Skill: ${data.name}</h3>
1787
- <p style="color:var(--text-dim)">${data.description}</p>
1788
- </div>
1789
- <textarea id="edit-skill-content" style="width:100%;height:60vh;font-family:monospace;font-size:0.875rem;padding:0.5rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary)">${escapeHtml(data.content)}</textarea>
1790
- <div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end">
1791
- <button class="btn" onclick="closeDrawer()">取消</button>
1792
- <button class="btn btn-primary" onclick="saveSkill('${name}')">💾 保存</button>
1793
- </div>
1794
- <div id="edit-skill-msg" style="margin-top:0.5rem;padding:0.5rem;border-radius:var(--radius-sm);display:none"></div>
1795
- `;
1796
- openDrawer(`编辑 Skill: ${name}`, html);
1797
- } catch (err) {
1798
- alert('加载失败: ' + err.message);
1799
- }
1800
- }
1801
-
1802
- async function saveSkill(name) {
1803
- const content = document.getElementById('edit-skill-content').value;
1804
- const msgEl = document.getElementById('edit-skill-msg');
1805
- try {
1806
- const r = await fetch(API + `/api/skills/${name}`, {
1807
- method: 'PUT',
1808
- headers: { 'Content-Type': 'application/json' },
1809
- body: JSON.stringify({ content }),
1810
- });
1811
- if (!r.ok) throw new Error('Failed to save skill');
1812
- const data = await r.json();
1813
- msgEl.textContent = `✓ 保存成功!备份: ${data.backup}`;
1814
- msgEl.style.background = 'var(--primary)';
1815
- msgEl.style.color = 'white';
1816
- msgEl.style.display = 'block';
1817
- setTimeout(() => { closeDrawer(); loadSkills(); }, 1500);
1818
- } catch (err) {
1819
- msgEl.textContent = `保存失败: ${err.message}`;
1820
- msgEl.style.background = 'var(--red)';
1821
- msgEl.style.color = 'white';
1822
- msgEl.style.display = 'block';
1823
- }
1824
- }
1825
-
1826
- function escapeHtml(text) {
1827
- const div = document.createElement('div');
1828
- div.textContent = text;
1829
- return div.innerHTML;
1830
- }
1831
-
1832
- // === Execution Trace ===
1833
- let allTraces = [];
1834
-
1835
- async function loadExecutionTraces() {
1836
- try {
1837
- const agentFilter = document.getElementById('trace-agent-filter').value;
1838
- const obeyedFilter = document.getElementById('trace-obeyed-filter').value;
1839
- const params = new URLSearchParams({ limit: '100' });
1840
- if (agentFilter) params.set('agent', agentFilter);
1841
- if (obeyedFilter) params.set('obeyed', obeyedFilter);
1842
-
1843
- const r = await fetch(API + '/api/execution-trace?' + params.toString());
1844
- if (!r.ok) throw new Error('Failed to load traces');
1845
- const data = await r.json();
1846
- allTraces = data.traces;
1847
-
1848
- // Populate agent filter dropdown
1849
- const agentSet = new Set();
1850
- allTraces.forEach(t => {
1851
- if (t.routedToName) agentSet.add(t.routedToName);
1852
- });
1853
- const agentSelect = document.getElementById('trace-agent-filter');
1854
- const currentAgent = agentSelect.value;
1855
- agentSelect.innerHTML = '<option value="">全部 Agent/Skill</option>' +
1856
- Array.from(agentSet).sort().map(a =>
1857
- `<option value="${a}" ${a === currentAgent ? 'selected' : ''}>${a}</option>`
1858
- ).join('');
1859
-
1860
- renderTraces();
1861
- ensureTraceStream();
1862
- } catch (err) {
1863
- console.error('Failed to load traces:', err);
1864
- }
1865
- }
1866
-
1867
- function ensureTraceStream() {
1868
- if (traceStream) return;
1869
- traceStream = new EventSource(API + '/api/execution-trace/stream');
1870
- traceStream.onmessage = (e) => {
1871
- try {
1872
- const data = JSON.parse(e.data);
1873
- if (data.type === 'execution-status') {
1874
- appendLiveStatus(data);
1875
- if (traceRefreshTimer) clearTimeout(traceRefreshTimer);
1876
- traceRefreshTimer = setTimeout(() => loadExecutionTraces(), 500);
1877
- }
1878
- } catch (err) {
1879
- console.error('Failed to parse SSE:', err);
1880
- }
1881
- };
1882
- traceStream.onerror = () => {
1883
- traceStream.close();
1884
- traceStream = null;
1885
- };
1886
- }
1887
-
1888
- function appendLiveStatus(data) {
1889
- const statusBox = document.getElementById('trace-live-status');
1890
- if (!statusBox) return;
1891
- const time = new Date(data.timestamp).toLocaleTimeString('zh-CN');
1892
- const statusBadge = data.status === 'routing' ? '<span class="badge badge-info">正在路由</span>'
1893
- : data.status === 'executing' ? '<span class="badge badge-warn">正在执行</span>'
1894
- : data.status === 'completed' ? '<span class="badge badge-allow">已完成</span>'
1895
- : '<span class="badge badge-block">失败</span>';
1896
- const item = `<div style="padding:0.5rem;border-bottom:1px solid var(--border)">${time} · Session ${data.sessionId.slice(0,8)} · ${statusBadge}${data.tool ? ` · ${data.tool}` : ''}${data.error ? ` · ${data.error}` : ''}</div>`;
1897
- statusBox.innerHTML = item + statusBox.innerHTML;
1898
- const children = statusBox.children;
1899
- while (children.length > 20) statusBox.removeChild(children[children.length - 1]);
1900
- }
1901
-
1902
- function renderTraces() {
1903
- const searchTerm = document.getElementById('trace-search').value.toLowerCase();
1904
- const filtered = allTraces.filter(t =>
1905
- !searchTerm || t.prompt.toLowerCase().includes(searchTerm)
1906
- );
1907
-
1908
- const list = document.getElementById('trace-list');
1909
- if (filtered.length === 0) {
1910
- list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-dim)">暂无执行追踪记录</div>';
1911
- return;
1912
- }
1913
-
1914
- const items = filtered.map(t => {
1915
- const time = new Date(t.timestamp).toLocaleString('zh-CN');
1916
- const obeyedBadge = !t.routedToType || !t.routedToName
1917
- ? '<span class="badge badge-info">— 未路由</span>'
1918
- : t.obeyed === 1
1919
- ? '<span class="badge badge-allow">✓ 遵守</span>'
1920
- : t.obeyed === 0
1921
- ? '<span class="badge badge-block">✗ 违抗</span>'
1922
- : t.completionReason === 'timeout'
1923
- ? '<span class="badge badge-warn">⏱ 已超时</span>'
1924
- : '<span class="badge badge-info">— 未判定</span>';
1925
-
1926
- const routeBadge = t.routedToName
1927
- ? `<span class="badge badge-info">${t.routedToType === 'agent' ? '🤖' : '⚡'} ${t.routedToName}</span>`
1928
- : '<span class="badge badge-warn">无路由</span>';
1929
-
1930
- const intentBadge = t.intent.taskType
1931
- ? `<span class="badge" style="background:var(--bg-secondary)">${t.intent.taskType}</span>`
1932
- : '';
1933
-
1934
- const firstToolBadge = t.firstTool
1935
- ? `<span class="badge" style="background:var(--bg-secondary)">首工具: ${t.firstTool}</span>`
1936
- : '';
1937
-
1938
- const classificationBadge = t.fallbackUsed
1939
- ? '<span class="badge badge-warn">正则兜底</span>'
1940
- : t.classificationMs
1941
- ? `<span class="badge badge-allow">AI 分类 (${t.classificationMs}ms)</span>`
1942
- : '';
1943
-
1944
- const executionBadge = t.totalExecutionMs
1945
- ? `<span class="badge" style="background:var(--purple-soft);color:var(--purple)">真实耗时: ${t.totalExecutionMs}ms</span>`
1946
- : '';
1947
-
1948
- const completionBadge = t.completionReason
1949
- ? `<span class="badge" style="background:var(--bg-secondary)">完成来源: ${t.completionReason}</span>`
1950
- : '';
1951
-
1952
- return `
1953
- <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:1rem;margin-bottom:0.75rem;cursor:pointer;transition:background 0.2s"
1954
- onclick="viewTraceDetails(${t.id})"
1955
- onmouseover="this.style.background='var(--bg-secondary)'"
1956
- onmouseout="this.style.background='transparent'">
1957
- <div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:0.5rem">
1958
- <div style="flex:1">
1959
- <div style="font-weight:500;margin-bottom:0.25rem">${escapeHtml(t.prompt.slice(0, 100))}${t.prompt.length > 100 ? '...' : ''}</div>
1960
- <div style="font-size:0.75rem;color:var(--text-dim)">${time} · Session: ${t.sessionId.slice(0, 8)}</div>
1961
- </div>
1962
- <div style="display:flex;gap:0.25rem;flex-wrap:wrap;justify-content:flex-end">
1963
- ${obeyedBadge}
1964
- ${routeBadge}
1965
- </div>
1966
- </div>
1967
- <div style="display:flex;gap:0.5rem;flex-wrap:wrap;font-size:0.75rem">
1968
- ${intentBadge}
1969
- ${firstToolBadge}
1970
- ${classificationBadge}
1971
- ${executionBadge}
1972
- ${completionBadge}
1973
- ${t.taskChain.length > 0 ? `<span class="badge badge-info">子 Agent: ${t.taskChain.join(' → ')}</span>` : ''}
1974
- </div>
1975
- </div>
1976
- `;
1977
- }).join('');
1978
-
1979
- list.innerHTML = items;
1980
- }
1981
-
1982
- function filterTraces() {
1983
- renderTraces();
1984
- }
1985
-
1986
- async function viewTraceDetails(traceId) {
1987
- try {
1988
- const r = await fetch(API + `/api/execution-trace/${traceId}`);
1989
- if (!r.ok) throw new Error('Failed to load trace details');
1990
- const data = await r.json();
1991
-
1992
- const routing = data.routing;
1993
- const sessionId = routing.sessionId;
1994
- const obeyedText = !routing.routedToType || !routing.routedToName
1995
- ? '<span class="badge badge-info">— 未路由</span>'
1996
- : routing.obeyed === 1
1997
- ? '<span class="badge badge-allow">✓ 遵守</span>'
1998
- : routing.obeyed === 0
1999
- ? '<span class="badge badge-block">✗ 违抗</span>'
2000
- : routing.completionReason === 'timeout'
2001
- ? '<span class="badge badge-warn">⏱ 已超时</span>'
2002
- : '<span class="badge badge-info">— 未判定</span>';
2003
-
2004
- const html = `
2005
- <div style="margin-bottom:1.5rem">
2006
- <h3>执行追踪详情</h3>
2007
- <div style="font-size:0.875rem;color:var(--text-dim);margin-top:0.5rem">Trace #${routing.id} · Session: ${sessionId}</div>
2008
- </div>
2009
-
2010
- <div style="margin-bottom:1.5rem">
2011
- <h4 style="margin-bottom:0.5rem">用户 Prompt</h4>
2012
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2013
- ${escapeHtml(routing.prompt)}
2014
- </div>
2015
- </div>
2016
-
2017
- <div style="margin-bottom:1.5rem">
2018
- <h4 style="margin-bottom:0.5rem">AI 意图分类</h4>
2019
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2020
- <div><strong>任务类型:</strong> ${routing.intent.taskType || '—'}</div>
2021
- <div><strong>复杂度:</strong> ${routing.intent.complexity || '—'}</div>
2022
- <div><strong>分类耗时:</strong> ${routing.classificationMs ? routing.classificationMs + 'ms' : '—'}</div>
2023
- <div><strong>分类方式:</strong> ${routing.fallbackUsed ? '<span class="badge badge-warn">正则兜底</span>' : '<span class="badge badge-allow">AI 分类</span>'}</div>
2024
- ${routing.intent.reasoning ? `<div style="margin-top:0.5rem"><strong>推理:</strong> ${escapeHtml(routing.intent.reasoning)}</div>` : ''}
2025
- </div>
2026
- </div>
2027
-
2028
- <div style="margin-bottom:1.5rem">
2029
- <h4 style="margin-bottom:0.5rem">路由决策</h4>
2030
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2031
- <div><strong>路由类型:</strong> ${routing.routedToType || '无'}</div>
2032
- <div><strong>路由目标:</strong> ${routing.routedToName || '—'}</div>
2033
- <div><strong>执行状态:</strong> ${obeyedText}</div>
2034
- <div><strong>完成来源:</strong> ${routing.completionReason || '—'}</div>
2035
- ${routing.refusalReason ? `<div style="margin-top:0.5rem;color:var(--red)"><strong>违抗原因:</strong> ${escapeHtml(routing.refusalReason)}</div>` : ''}
2036
- </div>
2037
- </div>
2038
-
2039
- <div style="margin-bottom:1.5rem">
2040
- <h4 style="margin-bottom:0.5rem">时间分解</h4>
2041
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2042
- <div><strong>路由开始:</strong> ${routing.timestamp ? new Date(routing.timestamp).toLocaleString('zh-CN') : '—'}</div>
2043
- <div><strong>首工具时间:</strong> ${routing.firstToolTs ? new Date(routing.firstToolTs).toLocaleString('zh-CN') : '—'}</div>
2044
- <div><strong>执行完成:</strong> ${routing.completedTs ? new Date(routing.completedTs).toLocaleString('zh-CN') : '—'}</div>
2045
- <div><strong>分类耗时:</strong> ${routing.classificationMs ? routing.classificationMs + 'ms' : '—'}</div>
2046
- <div><strong>首工具延迟:</strong> ${routing.firstToolTs ? (routing.firstToolTs - routing.timestamp) + 'ms' : '—'}</div>
2047
- <div><strong>真实执行耗时:</strong> ${routing.totalExecutionMs ? routing.totalExecutionMs + 'ms' : '—'}</div>
2048
- </div>
2049
- </div>
2050
-
2051
- <div style="margin-bottom:1.5rem">
2052
- <h4 style="margin-bottom:0.5rem">执行证明链</h4>
2053
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2054
- <div style="margin-bottom:0.5rem;color:var(--text-dim);font-family:monospace">route_request_id: ${routing.routeRequestId || '—'}</div>
2055
- ${(data.agentCalls && data.agentCalls.length > 0) ? data.agentCalls.map(c => {
2056
- const statusBadge = c.status === 'completed'
2057
- ? '<span class="badge badge-allow">已完成</span>'
2058
- : '<span class="badge badge-warn">已开始</span>';
2059
- const started = c.startedTs ? new Date(c.startedTs).toLocaleTimeString('zh-CN') : '—';
2060
- const completed = c.completedTs ? new Date(c.completedTs).toLocaleTimeString('zh-CN') : '—';
2061
- return `<div style="padding:0.5rem 0;border-bottom:1px solid var(--border)">
2062
- <div><strong>${c.tool}${c.subagent ? ' → ' + c.subagent : ''}</strong> ${statusBadge}</div>
2063
- <div style="color:var(--text-dim);font-size:0.75rem">开始: ${started} · 完成: ${completed}${c.error ? ' · 错误: ' + escapeHtml(c.error) : ''}</div>
2064
- </div>`;
2065
- }).join('') : empty('本次路由暂无 Agent 调用证据')}
2066
- </div>
2067
- </div>
2068
-
2069
- <div style="margin-bottom:1.5rem">
2070
- <h4 style="margin-bottom:0.5rem">实际执行</h4>
2071
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2072
- <div><strong>首个工具:</strong> ${routing.firstTool || '—'}</div>
2073
- ${routing.taskChain.length > 0 ? `<div><strong>子 Agent 链:</strong> ${routing.taskChain.join(' → ')}</div>` : ''}
2074
- </div>
2075
- </div>
2076
-
2077
- <div style="margin-bottom:1.5rem">
2078
- <h4 style="margin-bottom:0.5rem">事件时间线 (${data.events.length} 个事件)</h4>
2079
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow-y:auto;font-size:0.875rem">
2080
- ${data.events.map(e => `
2081
- <div style="padding:0.25rem 0;border-bottom:1px solid var(--border)">
2082
- <span style="color:var(--text-dim)">${new Date(e.timestamp).toLocaleTimeString('zh-CN')}</span>
2083
- <span class="badge badge-info" style="margin:0 0.5rem">${e.hook_type}</span>
2084
- ${e.tool_name ? `<span class="badge">${e.tool_name}</span>` : ''}
2085
- </div>
2086
- `).join('')}
2087
- </div>
2088
- </div>
2089
-
2090
- ${data.injections.length > 0 ? `
2091
- <div style="margin-bottom:1.5rem">
2092
- <h4 style="margin-bottom:0.5rem">注入内容 (${data.injections.length} 条)</h4>
2093
- <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow-y:auto;font-size:0.875rem">
2094
- ${data.injections.map(inj => `
2095
- <div style="padding:0.5rem 0;border-bottom:1px solid var(--border)">
2096
- <div><span class="badge badge-info">${inj.injection_type}</span></div>
2097
- <div style="margin-top:0.25rem;white-space:pre-wrap;font-family:monospace;font-size:0.8rem">${escapeHtml(inj.content.slice(0, 200))}${inj.content.length > 200 ? '...' : ''}</div>
2098
- </div>
2099
- `).join('')}
2100
- </div>
2101
- </div>
2102
- ` : ''}
2103
- `;
2104
-
2105
- openDrawer('执行追踪详情', html);
2106
- } catch (err) {
2107
- alert('加载失败: ' + err.message);
2108
- }
2109
- }
2110
-
2111
- function routingWindow() {
2112
- const el = document.getElementById('routing-window');
2113
- return el ? el.value : '168';
2114
- }
2115
-
2116
- function pct(n) { return n == null ? '—' : (n * 100).toFixed(1) + '%'; }
2117
- function ms(n) { return n == null ? '—' : n + 'ms'; }
2118
-
2119
- async function loadRoutingOverview() {
2120
- try {
2121
- const r = await fetch(API + '/api/routing/stats?window=' + routingWindow());
2122
- const data = await r.json();
2123
-
2124
- const statusColor =
2125
- data.obedienceRate == null ? 'badge-info' :
2126
- data.obedienceRate >= 0.8 ? 'badge-allow' :
2127
- data.obedienceRate >= 0.6 ? 'badge-warn' : 'badge-block';
2128
-
2129
- document.getElementById('routing-cards').innerHTML = [
2130
- card('事件总数', data.total),
2131
- card('强路由次数', data.forced),
2132
- card('听话率', pct(data.obedienceRate), statusColor),
2133
- card('违抗率', pct(data.refusalRate)),
2134
- card('兜底比例', pct(data.fallbackRate)),
2135
- card('AI 分类 p95', ms(data.latency.p95)),
2136
- ].join('');
2137
-
2138
- const byAgent = Object.entries(data.byAgent || {})
2139
- .sort((a, b) => b[1].total - a[1].total);
2140
- document.getElementById('routing-by-agent').innerHTML = byAgent.length === 0
2141
- ? empty('暂无数据')
2142
- : '<table><thead><tr><th>Agent</th><th>总数</th><th>遵守</th><th>违抗</th><th>未判定</th><th>听话率</th></tr></thead><tbody>'
2143
- + byAgent.map(([name, b]) => {
2144
- const rate = (b.obeyed + b.refused) === 0 ? null : b.obeyed / (b.obeyed + b.refused);
2145
- return '<tr><td style="font-family:monospace">' + name + '</td><td>' + b.total + '</td>'
2146
- + '<td style="color:var(--primary)">' + b.obeyed + '</td>'
2147
- + '<td style="color:var(--danger,#dc2626)">' + b.refused + '</td>'
2148
- + '<td style="color:var(--text-dim)">' + b.unknown + '</td>'
2149
- + '<td>' + pct(rate) + '</td></tr>';
2150
- }).join('')
2151
- + '</tbody></table>';
2152
-
2153
- const byVersion = Object.entries(data.byVersion || {});
2154
- document.getElementById('routing-by-version').innerHTML = byVersion.length === 0
2155
- ? empty('暂无版本数据')
2156
- : '<table><thead><tr><th>版本</th><th>总数</th><th>遵守</th><th>听话率</th></tr></thead><tbody>'
2157
- + byVersion.map(([v, b]) => '<tr><td style="font-family:monospace;font-size:0.8rem">' + v
2158
- + '</td><td>' + b.total + '</td><td>' + b.obeyed + '</td><td>'
2159
- + pct(b.total === 0 ? null : b.obeyed / b.total) + '</td></tr>').join('')
2160
- + '</tbody></table>';
2161
- } catch (err) {
2162
- document.getElementById('routing-cards').innerHTML = empty('加载失败: ' + err.message);
2163
- }
2164
- }
2165
-
2166
- function card(label, value, color) {
2167
- return '<div class="panel" style="padding:1rem">'
2168
- + '<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.25rem">' + label + '</div>'
2169
- + '<div style="font-size:1.5rem;font-weight:600' + (color ? ';color:var(--primary)' : '') + '">' + value + '</div>'
2170
- + '</div>';
2171
- }
2172
-
2173
- async function loadRoutingPerformance() {
2174
- try {
2175
- const r = await fetch(API + '/api/routing/performance?window=' + routingWindow() + '&minAttempts=10');
2176
- const data = await r.json();
2177
-
2178
- document.getElementById('routing-performance-cards').innerHTML = [
2179
- card('总路由数', data.summary.totalRouted),
2180
- card('已判定', data.summary.totalJudged),
2181
- card('遵守率', pct(data.summary.obedienceRate)),
2182
- card('违抗率', pct(data.summary.refusalRate)),
2183
- card('平均分类耗时', ms(data.summary.avgClassificationMs)),
2184
- card('平均真实耗时', ms(data.summary.avgExecutionMs)),
2185
- ].join('');
2186
-
2187
- const byAgentTbody = document.getElementById('routing-performance-by-agent');
2188
- byAgentTbody.innerHTML = (data.byAgent || []).length === 0
2189
- ? '<tr><td colspan="7">' + empty('暂无性能数据') + '</td></tr>'
2190
- : data.byAgent.map(a => '<tr>'
2191
- + '<td style="font-family:monospace">' + a.agent + '</td>'
2192
- + '<td>' + a.total + '</td>'
2193
- + '<td>' + a.judged + '</td>'
2194
- + '<td>' + pct(a.obedienceRate) + '</td>'
2195
- + '<td>' + pct(a.refusalRate) + '</td>'
2196
- + '<td>' + ms(a.avgClassificationMs) + '</td>'
2197
- + '<td>' + ms(a.avgExecutionMs) + '</td>'
2198
- + '</tr>').join('');
2199
-
2200
- const highRefusal = document.getElementById('routing-high-refusal');
2201
- highRefusal.innerHTML = (data.highRefusalAgents || []).length === 0
2202
- ? empty('暂无达到阈值的高违抗率 Agent')
2203
- : '<table><thead><tr><th>Agent</th><th>样本</th><th>违抗率</th><th>分类耗时</th><th>真实耗时</th></tr></thead><tbody>'
2204
- + data.highRefusalAgents.map(a => '<tr>'
2205
- + '<td style="font-family:monospace">' + a.agent + '</td>'
2206
- + '<td>' + a.totalAttempts + '</td>'
2207
- + '<td>' + pct(a.refusalRate) + '</td>'
2208
- + '<td>' + ms(a.avgClassificationMs) + '</td>'
2209
- + '<td>' + ms(a.avgExecutionMs) + '</td>'
2210
- + '</tr>').join('')
2211
- + '</tbody></table>';
2212
-
2213
- renderRoutingPerformanceCharts(data.dailyTrend || []);
2214
- } catch (err) {
2215
- document.getElementById('routing-performance-cards').innerHTML = empty('加载失败: ' + err.message);
2216
- document.getElementById('routing-performance-by-agent').innerHTML = '<tr><td colspan="7">' + empty('加载失败: ' + err.message) + '</td></tr>';
2217
- document.getElementById('routing-high-refusal').innerHTML = empty('加载失败: ' + err.message);
2218
- }
2219
- }
2220
-
2221
- function renderRoutingPerformanceCharts(trend) {
2222
- const labels = trend.map(d => d.date.slice(5));
2223
- const total = trend.map(d => d.total);
2224
- const obeyed = trend.map(d => d.obeyed);
2225
- const refused = trend.map(d => d.refused);
2226
- const latency = trend.map(d => d.avgExecutionMs ?? 0);
2227
-
2228
- if (charts.routingTrend) charts.routingTrend.destroy();
2229
- if (charts.routingLatency) charts.routingLatency.destroy();
2230
-
2231
- const trendCtx = document.getElementById('chart-routing-trend');
2232
- if (trendCtx) {
2233
- charts.routingTrend = new Chart(trendCtx, {
2234
- type: 'line',
2235
- data: {
2236
- labels,
2237
- datasets: [
2238
- { label: '总路由', data: total, borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.12)', tension: 0.25 },
2239
- { label: '遵守', data: obeyed, borderColor: '#16a34a', backgroundColor: 'rgba(22,163,74,0.12)', tension: 0.25 },
2240
- { label: '违抗', data: refused, borderColor: '#dc2626', backgroundColor: 'rgba(220,38,38,0.12)', tension: 0.25 },
2241
- ],
2242
- },
2243
- options: { responsive: true, maintainAspectRatio: false },
2244
- });
2245
- }
2246
-
2247
- const latencyCtx = document.getElementById('chart-routing-latency');
2248
- if (latencyCtx) {
2249
- charts.routingLatency = new Chart(latencyCtx, {
2250
- type: 'bar',
2251
- data: {
2252
- labels,
2253
- datasets: [{ label: '平均真实执行耗时(ms)', data: latency, backgroundColor: 'rgba(37,99,235,0.65)' }],
2254
- },
2255
- options: { responsive: true, maintainAspectRatio: false },
2256
- });
2257
- }
2258
- }
2259
-
2260
- async function loadRoutingAIOptimization() {
2261
- const container = document.getElementById('routing-ai-optimization');
2262
- container.innerHTML = loading();
2263
- try {
2264
- const minAttempts = parseInt(document.getElementById('ai-opt-min-attempts').value) || 10;
2265
- const r = await fetch(API + '/api/routing/ai-optimization?window=' + routingWindow() + '&minAttempts=' + minAttempts);
2266
- if (!r.ok) {
2267
- const err = await r.json().catch(() => ({ error: r.statusText }));
2268
- throw new Error(err.error || r.statusText);
2269
- }
2270
- const data = await r.json();
2271
- currentAIOptimizationData = data;
2272
-
2273
- const priorities = (data.priorities || []).length === 0
2274
- ? empty('暂无优先级建议')
2275
- : (data.priorities || []).map((p, idx) => `
2276
- <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
2277
- <div style="display:flex;justify-content:space-between;gap:0.5rem;align-items:center;margin-bottom:0.35rem">
2278
- <strong>#${idx + 1} ${escapeHtml(p.area || '优化项')}</strong>
2279
- <span class="badge badge-info">${Math.round((p.confidence || 0) * 100)}%</span>
2280
- </div>
2281
- <div style="font-size:0.875rem;margin-bottom:0.25rem"><strong>发现:</strong>${escapeHtml(p.finding || '')}</div>
2282
- <div style="font-size:0.875rem"><strong>影响:</strong>${escapeHtml(p.impact || '')}</div>
2283
- </div>
2284
- `).join('');
2285
-
2286
- const changes = (data.suggestedChanges || []).length === 0
2287
- ? empty('暂无可执行改动建议')
2288
- : (data.suggestedChanges || []).map((c, idx) => `
2289
- <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
2290
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
2291
- <strong>#${idx + 1} ${escapeHtml(c.targetType || '')}: ${escapeHtml(c.targetName || '—')}</strong>
2292
- <div style="display:flex;gap:0.5rem">
2293
- <span class="badge ${c.targetType === 'routing_rule' ? 'badge-warn' : 'badge-allow'}">${escapeHtml(c.targetType || '—')}</span>
2294
- <button class="btn" style="font-size:0.75rem;padding:0.25rem 0.5rem" onclick="previewPatch(${idx})">预览 Patch</button>
2295
- </div>
2296
- </div>
2297
- <div style="font-size:0.875rem;margin-bottom:0.25rem"><strong>建议:</strong>${escapeHtml(c.recommendation || '')}</div>
2298
- <div style="font-size:0.875rem;margin-bottom:0.25rem"><strong>理由:</strong>${escapeHtml(c.rationale || '')}</div>
2299
- <div style="font-size:0.875rem;color:var(--text-dim)"><strong>收益:</strong>${escapeHtml(c.expectedBenefit || '')}</div>
2300
- </div>
2301
- `).join('');
2302
-
2303
- const evidence = data.evidence || {};
2304
- container.innerHTML = `
2305
- <div class="cards" style="margin-bottom:1rem">
2306
- ${card('高违抗率 Agent', (evidence.highRefusalAgents || []).length)}
2307
- ${card('违抗模式', (evidence.topViolations || []).length)}
2308
- ${card('规则建议', (evidence.ruleRecommendations || []).length)}
2309
- </div>
2310
- <div class="grid-2">
2311
- <div class="panel">
2312
- <div class="panel-header"><span class="panel-title">AI 摘要</span></div>
2313
- <div class="panel-body" style="white-space:pre-wrap;line-height:1.6">${escapeHtml(data.summary || '—')}</div>
2314
- </div>
2315
- <div class="panel">
2316
- <div class="panel-header"><span class="panel-title">优先级</span></div>
2317
- <div class="panel-body">${priorities}</div>
2318
- </div>
2319
- </div>
2320
- <div class="grid-2" style="margin-top:1rem">
2321
- <div class="panel">
2322
- <div class="panel-header"><span class="panel-title">可执行改动建议</span></div>
2323
- <div class="panel-body">${changes}</div>
2324
- </div>
2325
- <div class="panel">
2326
- <div class="panel-header"><span class="panel-title">证据</span></div>
2327
- <div class="panel-body">
2328
- <details open style="margin-bottom:0.5rem"><summary style="cursor:pointer;font-weight:600">高违抗率 Agent</summary><pre style="white-space:pre-wrap;font-size:0.8rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px">${escapeHtml(JSON.stringify(evidence.highRefusalAgents || [], null, 2))}</pre></details>
2329
- <details style="margin-bottom:0.5rem"><summary style="cursor:pointer;font-weight:600">违抗模式</summary><pre style="white-space:pre-wrap;font-size:0.8rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px">${escapeHtml(JSON.stringify(evidence.topViolations || [], null, 2))}</pre></details>
2330
- <details><summary style="cursor:pointer;font-weight:600">规则建议</summary><pre style="white-space:pre-wrap;font-size:0.8rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px">${escapeHtml(JSON.stringify(evidence.ruleRecommendations || [], null, 2))}</pre></details>
2331
- </div>
2332
- </div>
2333
- </div>
2334
- `;
2335
- } catch (err) {
2336
- container.innerHTML = empty('加载失败: ' + err.message);
2337
- }
2338
- }
2339
-
2340
- // Patch preview and apply functions
2341
- async function previewPatch(idx) {
2342
- if (!currentAIOptimizationData || !currentAIOptimizationData.suggestedChanges) {
2343
- alert('请先加载 AI 优化建议');
2344
- return;
2345
- }
2346
- const change = currentAIOptimizationData.suggestedChanges[idx];
2347
- if (!change) {
2348
- alert('建议不存在');
2349
- return;
2350
- }
2351
-
2352
- const drawerTitle = document.getElementById('drawer-title');
2353
- const drawerBody = document.getElementById('drawer-body');
2354
- drawerTitle.textContent = 'Patch 预览';
2355
- drawerBody.innerHTML = '<div style="text-align:center;padding:2rem">正在生成 patch...</div>';
2356
- openDrawer();
2357
-
2358
- try {
2359
- const r = await fetch(API + '/api/patch/preview', {
2360
- method: 'POST',
2361
- headers: { 'Content-Type': 'application/json' },
2362
- body: JSON.stringify({
2363
- targetType: change.targetType,
2364
- targetName: change.targetName,
2365
- recommendation: change.recommendation,
2366
- rationale: change.rationale,
2367
- }),
2368
- });
2369
- if (!r.ok) {
2370
- const err = await r.json().catch(() => ({ error: r.statusText }));
2371
- throw new Error(err.error || r.statusText);
2372
- }
2373
- const preview = await r.json();
2374
- currentPatchPreview = preview;
2375
-
2376
- const riskBadge = preview.risk === 'low' ? '<span class="badge badge-allow">低风险</span>'
2377
- : preview.risk === 'high' ? '<span class="badge badge-block">高风险</span>'
2378
- : '<span class="badge badge-warn">中风险</span>';
2379
-
2380
- drawerBody.innerHTML = `
2381
- <div style="margin-bottom:1rem">
2382
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
2383
- <strong>${preview.targetType}: ${preview.targetName}</strong>
2384
- ${riskBadge}
2385
- </div>
2386
- <div style="font-size:0.875rem;color:var(--text-dim);margin-bottom:0.5rem">${escapeHtml(preview.summary)}</div>
2387
- <div style="font-size:0.875rem;color:var(--text-dim)">文件: ${escapeHtml(preview.filePath)}</div>
2388
- </div>
2389
- <div style="margin-bottom:1rem">
2390
- <strong>变更前:</strong>
2391
- <pre style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow:auto;font-size:0.75rem">${escapeHtml(preview.before)}</pre>
2392
- </div>
2393
- <div style="margin-bottom:1rem">
2394
- <strong>变更后:</strong>
2395
- <pre style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow:auto;font-size:0.75rem">${escapeHtml(preview.after)}</pre>
2396
- </div>
2397
- <div style="display:flex;gap:0.5rem;justify-content:flex-end">
2398
- <button class="btn" onclick="closeDrawer()">取消</button>
2399
- <button class="btn btn-primary" onclick="applyPatch()">应用 Patch</button>
2400
- </div>
2401
- `;
2402
- } catch (err) {
2403
- drawerBody.innerHTML = `<div style="color:var(--red);padding:1rem">预览失败: ${escapeHtml(err.message)}</div>`;
2404
- }
2405
- }
2406
-
2407
- async function applyPatch() {
2408
- if (!currentPatchPreview) {
2409
- alert('请先预览 patch');
2410
- return;
2411
- }
2412
-
2413
- if (!confirm(`确定要应用此 patch 吗?\n\n目标: ${currentPatchPreview.targetType}/${currentPatchPreview.targetName}\n\n系统会自动备份当前版本。`)) {
2414
- return;
2415
- }
2416
-
2417
- try {
2418
- const r = await fetch(API + '/api/patch/apply', {
2419
- method: 'POST',
2420
- headers: { 'Content-Type': 'application/json' },
2421
- body: JSON.stringify({
2422
- targetType: currentPatchPreview.targetType,
2423
- targetName: currentPatchPreview.targetName,
2424
- afterContent: currentPatchPreview.after,
2425
- }),
2426
- });
2427
- if (!r.ok) {
2428
- const err = await r.json().catch(() => ({ error: r.statusText }));
2429
- throw new Error(err.error || r.statusText);
2430
- }
2431
- const result = await r.json();
2432
- alert(`Patch 应用成功!\n\n备份路径: ${result.backupPath}\n\n请刷新相关页面查看结果。`);
2433
- closeDrawer();
2434
- const targetType = currentPatchPreview.targetType;
2435
- currentPatchPreview = null;
2436
-
2437
- // Refresh relevant pages
2438
- if (targetType === 'agent') loadAgents();
2439
- else if (targetType === 'skill') loadSkills();
2440
- else if (targetType === 'routing_rule') loadRoutingConfig();
2441
- } catch (err) {
2442
- alert('应用失败: ' + err.message);
2443
- }
2444
- }
2445
-
2446
- async function loadRoutingEvents() {
2447
- try {
2448
- const obeyed = document.getElementById('routing-filter-obeyed')?.value || '';
2449
- const agent = document.getElementById('routing-filter-agent')?.value || '';
2450
- const params = new URLSearchParams({ limit: '100' });
2451
- // 'forced' is a client-side filter: keep is_forced=1 rows regardless of obeyed.
2452
- if (obeyed !== '' && obeyed !== 'forced') params.set('obeyed', obeyed);
2453
- if (agent) params.set('agent', agent);
2454
- const r = await fetch(API + '/api/routing/events?' + params.toString());
2455
- let rows = await r.json();
2456
- if (obeyed === 'forced') rows = rows.filter(e => e.is_forced === 1);
2457
- const tbody = document.getElementById('routing-events-tbody');
2458
- if (!rows || rows.length === 0) {
2459
- tbody.innerHTML = '<tr><td colspan="5">' + empty('暂无路由事件') + '</td></tr>';
2460
- return;
2461
- }
2462
- tbody.innerHTML = rows.map(e => {
2463
- let status;
2464
- if (!e.is_forced) {
2465
- status = '<span class="badge" style="background:var(--bg-secondary);color:var(--text-dim)">未路由</span>';
2466
- } else if (e.obeyed === 1) {
2467
- status = '<span class="badge badge-allow">遵守</span>';
2468
- } else if (e.obeyed === 0) {
2469
- status = '<span class="badge badge-block">违抗</span>';
2470
- } else {
2471
- status = '<span class="badge badge-info">未判定</span>';
2472
- }
2473
- const ts = new Date(e.ts).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
2474
- return '<tr onclick="openRoutingEventDrawer(' + JSON.stringify(e).replace(/"/g,'&quot;') + ')">'
2475
- + '<td style="color:var(--text-dim);font-size:0.8rem">' + ts + '</td>'
2476
- + '<td style="font-family:monospace">' + (e.routed_to_name || '—') + '</td>'
2477
- + '<td>' + status + '</td>'
2478
- + '<td style="font-size:0.8rem;color:var(--text-dim)">' + (e.first_tool_name || '—') + '</td>'
2479
- + '<td style="font-size:0.8rem">' + (e.prompt || '').slice(0, 80) + '</td></tr>';
2480
- }).join('');
2481
- } catch (err) {
2482
- document.getElementById('routing-events-tbody').innerHTML = '<tr><td colspan="5">' + empty('加载失败: ' + err.message) + '</td></tr>';
2483
- }
2484
- }
2485
-
2486
- function openRoutingEventDrawer(e) {
2487
- let intent = {};
2488
- try { intent = JSON.parse(e.intent_json || '{}'); } catch { /* ignore */ }
2489
- let chain = [];
2490
- try { chain = JSON.parse(e.downstream_task_chain || '[]'); } catch { /* ignore */ }
2491
- const html = [
2492
- '<div style="margin-bottom:1rem"><div style="font-size:0.75rem;color:var(--text-dim)">Prompt</div>',
2493
- '<div style="white-space:pre-wrap;font-size:0.875rem">' + (e.prompt || '—') + '</div></div>',
2494
- '<div style="margin-bottom:1rem"><div style="font-size:0.75rem;color:var(--text-dim)">Intent</div>',
2495
- '<pre style="font-size:0.75rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;overflow:auto">'
2496
- + JSON.stringify(intent, null, 2) + '</pre></div>',
2497
- '<div style="margin-bottom:1rem"><div style="font-size:0.75rem;color:var(--text-dim)">路由</div>',
2498
- '<div>type=<b>' + (e.routed_to_type || '—') + '</b>, name=<b>' + (e.routed_to_name || '—')
2499
- + '</b>, is_forced=' + (e.is_forced ? 'yes' : 'no') + '</div></div>',
2500
- '<div style="margin-bottom:1rem"><div style="font-size:0.75rem;color:var(--text-dim)">判定</div>',
2501
- '<div>obeyed=<b>' + (e.obeyed === null || e.obeyed === undefined ? 'null' : e.obeyed)
2502
- + '</b>, first_tool=' + (e.first_tool_name || '—') + '</div></div>',
2503
- chain.length > 0 ? '<div style="margin-bottom:1rem"><div style="font-size:0.75rem;color:var(--text-dim)">下游 Task 链</div><code>'
2504
- + chain.join(' → ') + '</code></div>' : '',
2505
- e.refusal_reason ? '<div style="margin-bottom:1rem"><div style="font-size:0.75rem;color:var(--text-dim)">违抗原因</div>'
2506
- + '<div style="padding:0.5rem;background:var(--bg-secondary);border-left:3px solid var(--danger,#dc2626)">'
2507
- + e.refusal_reason + '</div></div>' : '',
2508
- '<div style="font-size:0.75rem;color:var(--text-dim)">classification_ms=' + (e.classification_ms || '—')
2509
- + ' · fallback_used=' + (e.fallback_used ? 'yes' : 'no')
2510
- + ' · injection_version=' + (e.injection_version || '—') + '</div>',
2511
- ].join('');
2512
- openDrawer('路由事件 #' + e.id, html);
2513
- }
2514
-
2515
- async function loadRoutingRefusals() {
2516
- try {
2517
- // Load refusals, violations, and rule-states in parallel.
2518
- const [refusalsRes, violationsRes, rulesRes] = await Promise.all([
2519
- fetch(API + '/api/routing/refusals?window=' + routingWindow()),
2520
- fetch(API + '/api/routing/violations?window=' + routingWindow()),
2521
- fetch(API + '/api/routing/rule-states'),
2522
- ]);
2523
- const refusalsData = await refusalsRes.json();
2524
- const violationsData = await violationsRes.json();
2525
- const ruleStatesData = await rulesRes.json();
2526
- const disabledMap = new Map();
2527
- (ruleStatesData.ruleStates || []).forEach(s => {
2528
- disabledMap.set(s.task_type + '__' + s.agent, s);
2529
- });
2530
-
2531
- const container = document.getElementById('routing-refusals');
2532
-
2533
- // Show violations alerts first (Phase 3 Feature 3)
2534
- let html = '';
2535
- if (violationsData.violations && violationsData.violations.length > 0) {
2536
- const criticalViolations = violationsData.violations.filter(v => v.severity === 'critical' || v.severity === 'high');
2537
- if (criticalViolations.length > 0) {
2538
- html += '<div style="margin-bottom:1rem;padding:1rem;background:var(--danger,#dc2626);color:white;border-radius:var(--radius-sm)">';
2539
- html += '<div style="font-weight:600;margin-bottom:0.5rem">⚠️ 检测到 ' + criticalViolations.length + ' 个高风险违抗模式</div>';
2540
- html += '<div style="font-size:0.875rem">以下路由规则的违抗率过高,建议调整或禁用:</div>';
2541
- html += '<ul style="margin:0.5rem 0 0 1.5rem;padding:0">';
2542
- criticalViolations.forEach(v => {
2543
- html += '<li style="margin-bottom:0.25rem"><code>' + v.taskType + '</code> → <code>' + v.agent + '</code> ';
2544
- html += '(违抗率 ' + (v.refusalRate * 100).toFixed(0) + '%, ' + v.refusals + '/' + v.totalAttempts + ' 次)</li>';
2545
- });
2546
- html += '</ul></div>';
2547
- }
2548
-
2549
- // Show all violations with severity badges
2550
- html += '<div style="margin-bottom:1rem"><div style="font-weight:600;margin-bottom:0.5rem">违抗模式分析</div>';
2551
- violationsData.violations.forEach(v => {
2552
- const severityColor = {
2553
- critical: 'background:#dc2626;color:white',
2554
- high: 'background:#f59e0b;color:white',
2555
- medium: 'background:#3b82f6;color:white',
2556
- low: 'background:var(--bg-secondary);color:var(--text-dim)',
2557
- }[v.severity];
2558
- const severityLabel = { critical: '严重', high: '高', medium: '中', low: '低' }[v.severity];
2559
-
2560
- html += '<details style="margin-bottom:0.5rem;border:1px solid var(--border);border-radius:4px;padding:0.5rem">';
2561
- html += '<summary style="cursor:pointer;display:flex;gap:0.5rem;align-items:center">';
2562
- html += '<span style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;font-weight:600;' + severityColor + '">' + severityLabel + '</span>';
2563
- html += '<code>' + v.taskType + '</code> → <code>' + v.agent + '</code>';
2564
- const state = disabledMap.get(v.taskType + '__' + v.agent);
2565
- if (state && state.disabled === 1) {
2566
- const tag = state.auto_disabled === 1 ? '自动禁用' : '手动禁用';
2567
- html += '<span style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;background:var(--text-dim);color:white">' + tag + '</span>';
2568
- }
2569
- html += '<span style="margin-left:auto;font-size:0.875rem;color:var(--text-dim)">违抗率 ' + (v.refusalRate * 100).toFixed(0) + '% (' + v.refusals + '/' + v.totalAttempts + ')</span>';
2570
- const isDisabled = state && state.disabled === 1;
2571
- const btnLabel = isDisabled ? '启用' : '禁用';
2572
- html += '<button class="btn" style="margin-left:0.5rem;padding:0.25rem 0.5rem;font-size:0.75rem" onclick="event.preventDefault();event.stopPropagation();toggleRuleState(\'' + v.taskType + '\', \'' + v.agent + '\', ' + (!isDisabled) + ')">' + btnLabel + '</button>';
2573
- html += '</summary>';
2574
- html += '<div style="margin-top:0.5rem;padding:0.5rem;background:var(--bg-secondary);border-radius:4px;font-size:0.875rem">';
2575
- html += '<div style="margin-bottom:0.5rem">最近 5 次尝试中违抗 ' + v.recentRefusals + ' 次</div>';
2576
- if (v.samples.length > 0) {
2577
- html += '<div style="font-weight:600;margin-bottom:0.25rem">样例:</div><ul style="margin:0;padding:0 0 0 1rem">';
2578
- v.samples.forEach(s => {
2579
- html += '<li style="margin-bottom:0.5rem">';
2580
- html += '<div>' + s.prompt + '</div>';
2581
- if (s.refusal_reason) {
2582
- html += '<div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">↳ ' + s.refusal_reason + '</div>';
2583
- }
2584
- html += '</li>';
2585
- });
2586
- html += '</ul>';
2587
- }
2588
- html += '</div></details>';
2589
- });
2590
- html += '</div>';
2591
- }
2592
-
2593
- // Show original refusals grouping (for backward compatibility)
2594
- if (!refusalsData.groups || refusalsData.groups.length === 0) {
2595
- if (html === '') {
2596
- container.innerHTML = empty('暂无违抗事件');
2597
- } else {
2598
- container.innerHTML = html;
2599
- }
2600
- return;
2601
- }
2602
-
2603
- html += '<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border)">';
2604
- html += '<div style="font-weight:600;margin-bottom:0.5rem">违抗事件列表</div>';
2605
- html += refusalsData.groups.map(g => {
2606
- const samples = g.samples.map(s =>
2607
- '<li style="margin-bottom:0.5rem">'
2608
- + '<div style="font-size:0.8rem">' + s.prompt + '</div>'
2609
- + (s.refusal_reason ? '<div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">↳ ' + s.refusal_reason + '</div>' : '')
2610
- + '</li>'
2611
- ).join('');
2612
- return '<details style="margin-bottom:0.5rem;border:1px solid var(--border);border-radius:4px;padding:0.5rem">'
2613
- + '<summary style="cursor:pointer;display:flex;gap:0.5rem;align-items:center">'
2614
- + '<span class="badge badge-block">' + g.count + '</span>'
2615
- + '<code>' + g.taskType + '</code> → <code>' + g.agent + '</code></summary>'
2616
- + '<ul style="margin:0.5rem 0 0 1rem;padding:0">' + samples + '</ul>'
2617
- + '</details>';
2618
- }).join('');
2619
- html += '</div>';
2620
-
2621
- container.innerHTML = html;
2622
- } catch (err) {
2623
- document.getElementById('routing-refusals').innerHTML = empty('加载失败: ' + err.message);
2624
- }
2625
- }
2626
-
2627
- async function toggleRuleState(taskType, agent, disabled) {
2628
- const verb = disabled ? '禁用' : '启用';
2629
- if (!confirm(verb + ' ' + taskType + ' → ' + agent + ' 的路由规则?')) return;
2630
- try {
2631
- const r = await fetch(API + '/api/routing/rule-states', {
2632
- method: 'PUT',
2633
- headers: { 'Content-Type': 'application/json' },
2634
- body: JSON.stringify({ taskType, agent, disabled, reason: disabled ? '手动禁用(Web UI)' : null }),
2635
- });
2636
- if (!r.ok) {
2637
- const err = await r.json();
2638
- throw new Error(err.error || 'Unknown error');
2639
- }
2640
- await loadRoutingRefusals();
2641
- } catch (err) {
2642
- alert(verb + '失败: ' + err.message);
2643
- }
2644
- }
2645
-
2646
- // === Routing Config Editor (Phase 3 Feature 2, upgraded to Monaco in Phase 4 Feature 1) ===
2647
-
2648
- let monacoEditor = null;
2649
-
2650
- function initMonacoEditor() {
2651
- if (monacoEditor) return; // already initialized
2652
-
2653
- require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
2654
- require(['vs/editor/editor.main'], function () {
2655
- monacoEditor = monaco.editor.create(document.getElementById('routing-config-editor'), {
2656
- value: '',
2657
- language: 'yaml',
2658
- theme: 'vs',
2659
- automaticLayout: true,
2660
- minimap: { enabled: false },
2661
- scrollBeyondLastLine: false,
2662
- fontSize: 14,
2663
- lineNumbers: 'on',
2664
- renderWhitespace: 'selection',
2665
- tabSize: 2,
2666
- });
2667
- });
2668
- }
2669
-
2670
- async function loadRoutingConfig() {
2671
- try {
2672
- const r = await fetch(API + '/api/routing/config');
2673
- const data = await r.json();
2674
-
2675
- // Initialize Monaco Editor if not already done
2676
- if (!monacoEditor) {
2677
- initMonacoEditor();
2678
- // Wait for Monaco to initialize
2679
- await new Promise(resolve => {
2680
- const checkInterval = setInterval(() => {
2681
- if (monacoEditor) {
2682
- clearInterval(checkInterval);
2683
- resolve();
2684
- }
2685
- }, 100);
2686
- });
2687
- }
2688
-
2689
- // Set editor content
2690
- if (monacoEditor) {
2691
- monacoEditor.setValue(data.content || '');
2692
- }
2693
-
2694
- const sourceLabel = data.source === 'user' ? '用户配置' : data.source === 'default' ? '默认配置' : '无配置';
2695
- document.getElementById('routing-config-source').textContent = sourceLabel;
2696
- document.getElementById('routing-config-path').textContent = data.source === 'user' ? data.userPath : data.defaultPath;
2697
-
2698
- // Hide error/success messages
2699
- document.getElementById('routing-config-error').style.display = 'none';
2700
- document.getElementById('routing-config-success').style.display = 'none';
2701
- } catch (err) {
2702
- document.getElementById('routing-config-error').textContent = '加载失败: ' + err.message;
2703
- document.getElementById('routing-config-error').style.display = 'block';
2704
- }
2705
- }
2706
-
2707
- async function saveRoutingConfig() {
2708
- if (!monacoEditor) {
2709
- alert('编辑器未初始化');
2710
- return;
2711
- }
2712
-
2713
- const content = monacoEditor.getValue();
2714
- const errorEl = document.getElementById('routing-config-error');
2715
- const successEl = document.getElementById('routing-config-success');
2716
-
2717
- errorEl.style.display = 'none';
2718
- successEl.style.display = 'none';
2719
-
2720
- try {
2721
- const r = await fetch(API + '/api/routing/config', {
2722
- method: 'PUT',
2723
- headers: { 'Content-Type': 'application/json' },
2724
- body: JSON.stringify({ content }),
2725
- });
2726
-
2727
- if (!r.ok) {
2728
- const err = await r.json();
2729
- throw new Error(err.error || 'Unknown error');
2730
- }
2731
-
2732
- const result = await r.json();
2733
- successEl.textContent = '✓ 保存成功!配置已写入 ' + result.path + '(热加载将在几秒内生效)';
2734
- successEl.style.display = 'block';
2735
-
2736
- // Reload to show updated source
2737
- setTimeout(() => loadRoutingConfig(), 1000);
2738
- } catch (err) {
2739
- errorEl.textContent = '保存失败: ' + err.message;
2740
- errorEl.style.display = 'block';
2741
- }
2742
- }
2743
-
2744
- // === A/B Testing (Phase 5 Feature 1) ===
2745
-
2746
- let experimentsEditor = null;
2747
-
2748
- function initExperimentsEditor() {
2749
- if (experimentsEditor) return;
2750
- require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
2751
- require(['vs/editor/editor.main'], function () {
2752
- experimentsEditor = monaco.editor.create(document.getElementById('experiments-config-editor'), {
2753
- value: '',
2754
- language: 'yaml',
2755
- theme: 'vs',
2756
- automaticLayout: true,
2757
- minimap: { enabled: false },
2758
- scrollBeyondLastLine: false,
2759
- fontSize: 14,
2760
- lineNumbers: 'on',
2761
- renderWhitespace: 'selection',
2762
- tabSize: 2,
2763
- });
2764
- });
2765
- }
2766
-
2767
- async function loadExperimentsConfig() {
2768
- try {
2769
- const r = await fetch(API + '/api/routing/experiments/config');
2770
- const data = await r.json();
2771
-
2772
- if (!experimentsEditor) {
2773
- initExperimentsEditor();
2774
- await new Promise(resolve => {
2775
- const tick = setInterval(() => {
2776
- if (experimentsEditor) { clearInterval(tick); resolve(); }
2777
- }, 100);
2778
- });
2779
- }
2780
-
2781
- experimentsEditor.setValue(data.content || '');
2782
- document.getElementById('experiments-config-source').textContent =
2783
- data.source === 'user' ? '用户配置' : '未配置(保存将创建新文件)';
2784
- document.getElementById('experiments-config-path').textContent = data.path || '';
2785
-
2786
- document.getElementById('experiments-config-error').style.display = 'none';
2787
- document.getElementById('experiments-config-success').style.display = 'none';
2788
- } catch (err) {
2789
- document.getElementById('experiments-config-error').textContent = '加载失败: ' + err.message;
2790
- document.getElementById('experiments-config-error').style.display = 'block';
2791
- }
2792
- }
2793
-
2794
- async function saveExperimentsConfig() {
2795
- if (!experimentsEditor) { alert('编辑器未初始化'); return; }
2796
- const content = experimentsEditor.getValue();
2797
- const errorEl = document.getElementById('experiments-config-error');
2798
- const successEl = document.getElementById('experiments-config-success');
2799
- errorEl.style.display = 'none';
2800
- successEl.style.display = 'none';
2801
-
2802
- try {
2803
- const r = await fetch(API + '/api/routing/experiments/config', {
2804
- method: 'PUT',
2805
- headers: { 'Content-Type': 'application/json' },
2806
- body: JSON.stringify({ content }),
2807
- });
2808
- if (!r.ok) {
2809
- const err = await r.json();
2810
- throw new Error(err.error || 'Unknown error');
2811
- }
2812
- const result = await r.json();
2813
- successEl.textContent = '✓ 保存成功!配置已写入 ' + result.path;
2814
- successEl.style.display = 'block';
2815
- setTimeout(() => { loadExperimentsConfig(); loadExperimentsAnalysis(); }, 1000);
2816
- } catch (err) {
2817
- errorEl.textContent = '保存失败: ' + err.message;
2818
- errorEl.style.display = 'block';
2819
- }
2820
- }
2821
-
2822
- async function loadExperimentsAnalysis() {
2823
- const summaryEl = document.getElementById('experiments-summary');
2824
- const tableEl = document.getElementById('experiments-analysis');
2825
- const winnerEl = document.getElementById('experiments-winner');
2826
- summaryEl.innerHTML = '';
2827
- tableEl.innerHTML = loading();
2828
- winnerEl.style.display = 'none';
2829
- winnerEl.innerHTML = '';
2830
-
2831
- try {
2832
- const r = await fetch(API + '/api/routing/experiments/analysis');
2833
- const data = await r.json();
2834
-
2835
- if (!data.experimentId) {
2836
- tableEl.innerHTML = empty('当前没有活跃实验。编辑上方 YAML 配置并保存后即可开始。');
2837
- return;
2838
- }
2839
-
2840
- const endedLabel = data.endedAt ? ' · 已结束 ' + fmt(data.endedAt) : (data.enabled ? ' · 进行中' : ' · 未启用');
2841
- summaryEl.innerHTML =
2842
- '<strong>' + (data.experimentName || data.experimentId) + '</strong>' +
2843
- '<span style="color:var(--text-dim)"> (' + data.experimentId + ')</span>' +
2844
- '<span style="color:var(--text-dim)">' + endedLabel + '</span>' +
2845
- '<br><small style="color:var(--text-dim)">开始于 ' + fmt(data.startedAt) + '</small>';
2846
-
2847
- const rows = (data.groups || []).map(g => {
2848
- const rate = g.obeyedRate == null ? '—' : (g.obeyedRate * 100).toFixed(1) + '%';
2849
- const avg = g.avgClassificationMs == null ? '—' : g.avgClassificationMs.toFixed(0) + 'ms';
2850
- return '<tr>' +
2851
- '<td><strong>' + g.id + '</strong> <span style="color:var(--text-dim)">(' + g.name + ')</span></td>' +
2852
- '<td>' + g.weight + '%</td>' +
2853
- '<td>' + g.total + '</td>' +
2854
- '<td>' + g.obeyed + '</td>' +
2855
- '<td>' + g.refused + '</td>' +
2856
- '<td>' + g.unknown + '</td>' +
2857
- '<td>' + rate + '</td>' +
2858
- '<td>' + avg + '</td>' +
2859
- '</tr>';
2860
- }).join('');
2861
-
2862
- tableEl.innerHTML =
2863
- '<table>' +
2864
- '<thead><tr>' +
2865
- '<th>组</th><th>权重</th><th>样本</th><th>听话</th><th>违抗</th><th>未知</th><th>听话率</th><th>平均分类耗时</th>' +
2866
- '</tr></thead><tbody>' + rows + '</tbody></table>';
2867
-
2868
- if (data.zScore !== null && data.zScore !== undefined) {
2869
- const zline =
2870
- '<div style="margin-top:0.5rem;font-size:0.875rem;color:var(--text-dim)">' +
2871
- 'z-score = ' + data.zScore.toFixed(3) +
2872
- (data.sampleAdequate ? ' (样本充足)' : ' (样本不足,需每组 ≥ 50)') +
2873
- '</div>';
2874
- tableEl.insertAdjacentHTML('beforeend', zline);
2875
- }
2876
-
2877
- if (data.suggestedWinner && !data.endedAt) {
2878
- winnerEl.style.display = 'block';
2879
- winnerEl.innerHTML =
2880
- '<div style="padding:1rem;background:var(--primary);color:white;border-radius:var(--radius-sm)">' +
2881
- '<div style="margin-bottom:0.5rem"><strong>建议获胜方案:' + data.suggestedWinner + '</strong> (z &gt; 1.96)</div>' +
2882
- '<button class="btn" style="background:white;color:var(--primary)" onclick="promoteExperiment(\'' + data.suggestedWinner + '\')">一键应用该组规则到 routing.yaml</button>' +
2883
- '</div>';
2884
- } else if (!data.endedAt && data.groups && data.groups.length >= 2) {
2885
- winnerEl.style.display = 'block';
2886
- const buttons = data.groups.map(g =>
2887
- '<button class="btn" onclick="promoteExperiment(\'' + g.id + '\')">手动晶升 ' + g.id + '</button>'
2888
- ).join(' ');
2889
- winnerEl.innerHTML =
2890
- '<div style="padding:1rem;background:var(--bg-alt);border:1px solid var(--border);border-radius:var(--radius-sm)">' +
2891
- '<div style="margin-bottom:0.5rem;color:var(--text-dim)">尚未达到显著差异。如确定要提前收尾实验:</div>' +
2892
- buttons + '</div>';
2893
- }
2894
- } catch (err) {
2895
- tableEl.innerHTML = empty('加载失败: ' + err.message);
2896
- }
2897
- }
2898
-
2899
- async function promoteExperiment(groupId) {
2900
- if (!confirm('将 ' + groupId + ' 组的规则写入 routing.yaml 并结束当前实验?原 routing.yaml 会备份为 .bak 文件。')) return;
2901
- try {
2902
- const r = await fetch(API + '/api/routing/experiments/promote', {
2903
- method: 'POST',
2904
- headers: { 'Content-Type': 'application/json' },
2905
- body: JSON.stringify({ groupId }),
2906
- });
2907
- if (!r.ok) {
2908
- const err = await r.json();
2909
- throw new Error(err.error || 'Unknown error');
2910
- }
2911
- const result = await r.json();
2912
- alert('✓ 已晶升 ' + result.promoted + ' 组到 routing.yaml' +
2913
- (result.backupPath ? '\n备份:' + result.backupPath : '') +
2914
- '\n实验结束于 ' + result.endedAt);
2915
- await loadExperimentsConfig();
2916
- await loadExperimentsAnalysis();
2917
- } catch (err) {
2918
- alert('晶升失败: ' + err.message);
2919
- }
2920
- }
2921
-
2922
- // === Rule Recommendations (Phase 5 Feature 3) ===
2923
-
2924
- async function loadRecommendations() {
2925
- const container = document.getElementById('recommendations-list');
2926
- container.innerHTML = loading();
2927
- const days = document.getElementById('recommendations-days')?.value || '7';
2928
- try {
2929
- const r = await fetch(API + '/api/routing/recommendations?days=' + days);
2930
- const data = await r.json();
2931
- const list = data.recommendations || [];
2932
- if (list.length === 0) {
2933
- container.innerHTML = empty('暂无推荐。样本量不足或当前规则已贴近实际行为。');
2934
- return;
2935
- }
2936
-
2937
- let html = '<table>';
2938
- html += '<thead><tr>' +
2939
- '<th>任务类型</th><th>复杂度</th>' +
2940
- '<th>当前路由</th><th>建议路由</th>' +
2941
- '<th>样本</th><th>当前听话率</th><th>建议 agent 使用率</th>' +
2942
- '<th>置信度</th><th>说明</th>' +
2943
- '</tr></thead><tbody>';
2944
- list.forEach(rec => {
2945
- const pctFmt = v => (v * 100).toFixed(0) + '%';
2946
- const conf = (rec.confidence * 100).toFixed(0) + '%';
2947
- html += '<tr>' +
2948
- '<td><code>' + rec.taskType + '</code></td>' +
2949
- '<td>' + (rec.complexity || '—') + '</td>' +
2950
- '<td>' + (rec.currentAgent ? '<code>' + rec.currentAgent + '</code>' : '<span style="color:var(--text-dim)">(skill 回退)</span>') + '</td>' +
2951
- '<td><code style="color:var(--primary)">' + rec.recommendedAgent + '</code></td>' +
2952
- '<td>' + rec.sampleSize + '</td>' +
2953
- '<td>' + pctFmt(rec.currentObeyedRate) + '</td>' +
2954
- '<td>' + pctFmt(rec.recommendedUsageRate) + '</td>' +
2955
- '<td>' + conf + '</td>' +
2956
- '<td style="font-size:0.85rem;color:var(--text-dim)">' + rec.reason + '</td>' +
2957
- '</tr>';
2958
- });
2959
- html += '</tbody></table>';
2960
- html += '<div style="margin-top:0.75rem;font-size:0.85rem;color:var(--text-dim)">' +
2961
- '要应用某条推荐,打开"路由调优"tab,手动更新对应 when 块的 agent 名称并保存。' +
2962
- '</div>';
2963
- container.innerHTML = html;
2964
- } catch (err) {
2965
- container.innerHTML = empty('加载推荐失败: ' + err.message);
2966
- }
2967
- }
2968
-
2969
- // === Init ===
2970
- nav('dashboard');
2971
- </script>
2972
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Claude Forge 管理后台</title>
8
+ <script type="module" crossorigin src="/assets/index-DnaQt27h.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CtylfoaN.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
2973
14
  </html>