@mengzf/skillflow-mcp 0.0.0-stage → 1.0.0

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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -2
  3. package/dist/adapters/workbuddy.sniffer.d.ts +22 -0
  4. package/dist/adapters/workbuddy.sniffer.d.ts.map +1 -0
  5. package/dist/adapters/workbuddy.sniffer.js +143 -0
  6. package/dist/adapters/workbuddy.sniffer.js.map +1 -0
  7. package/dist/config.d.ts +9 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +33 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/db/index.d.ts +4 -0
  12. package/dist/db/index.d.ts.map +1 -0
  13. package/dist/db/index.js +39 -0
  14. package/dist/db/index.js.map +1 -0
  15. package/dist/db/schema.d.ts +3 -0
  16. package/dist/db/schema.d.ts.map +1 -0
  17. package/dist/db/schema.js +103 -0
  18. package/dist/db/schema.js.map +1 -0
  19. package/dist/db/seed.d.ts +15 -0
  20. package/dist/db/seed.d.ts.map +1 -0
  21. package/dist/db/seed.js +128 -0
  22. package/dist/db/seed.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +51 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/server.d.ts +7 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +368 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/services/benchmark.service.d.ts +27 -0
  32. package/dist/services/benchmark.service.d.ts.map +1 -0
  33. package/dist/services/benchmark.service.js +94 -0
  34. package/dist/services/benchmark.service.js.map +1 -0
  35. package/dist/services/dashboard.service.d.ts +12 -0
  36. package/dist/services/dashboard.service.d.ts.map +1 -0
  37. package/dist/services/dashboard.service.js +191 -0
  38. package/dist/services/dashboard.service.js.map +1 -0
  39. package/dist/services/impact.service.d.ts +20 -0
  40. package/dist/services/impact.service.d.ts.map +1 -0
  41. package/dist/services/impact.service.js +114 -0
  42. package/dist/services/impact.service.js.map +1 -0
  43. package/dist/services/telemetry.service.d.ts +33 -0
  44. package/dist/services/telemetry.service.d.ts.map +1 -0
  45. package/dist/services/telemetry.service.js +175 -0
  46. package/dist/services/telemetry.service.js.map +1 -0
  47. package/dist/types/index.d.ts +3 -0
  48. package/dist/types/index.d.ts.map +1 -0
  49. package/dist/types/index.js +3 -0
  50. package/dist/types/index.js.map +1 -0
  51. package/dist/types/models.d.ts +61 -0
  52. package/dist/types/models.d.ts.map +1 -0
  53. package/dist/types/models.js +2 -0
  54. package/dist/types/models.js.map +1 -0
  55. package/dist/types/tools.d.ts +134 -0
  56. package/dist/types/tools.d.ts.map +1 -0
  57. package/dist/types/tools.js +149 -0
  58. package/dist/types/tools.js.map +1 -0
  59. package/dist/utils/ascii-chart.d.ts +9 -0
  60. package/dist/utils/ascii-chart.d.ts.map +1 -0
  61. package/dist/utils/ascii-chart.js +29 -0
  62. package/dist/utils/ascii-chart.js.map +1 -0
  63. package/dist/utils/formatter.d.ts +6 -0
  64. package/dist/utils/formatter.d.ts.map +1 -0
  65. package/dist/utils/formatter.js +29 -0
  66. package/dist/utils/formatter.js.map +1 -0
  67. package/dist/utils/hours.d.ts +3 -0
  68. package/dist/utils/hours.d.ts.map +1 -0
  69. package/dist/utils/hours.js +15 -0
  70. package/dist/utils/hours.js.map +1 -0
  71. package/dist/utils/pricing.d.ts +19 -0
  72. package/dist/utils/pricing.d.ts.map +1 -0
  73. package/dist/utils/pricing.js +96 -0
  74. package/dist/utils/pricing.js.map +1 -0
  75. package/dist/utils/quality.d.ts +15 -0
  76. package/dist/utils/quality.d.ts.map +1 -0
  77. package/dist/utils/quality.js +47 -0
  78. package/dist/utils/quality.js.map +1 -0
  79. package/dist/web/render.d.ts +2 -0
  80. package/dist/web/render.d.ts.map +1 -0
  81. package/dist/web/render.js +593 -0
  82. package/dist/web/render.js.map +1 -0
  83. package/dist/web/server.d.ts +10 -0
  84. package/dist/web/server.d.ts.map +1 -0
  85. package/dist/web/server.js +136 -0
  86. package/dist/web/server.js.map +1 -0
  87. package/package.json +51 -4
@@ -0,0 +1,593 @@
1
+ export function renderDashboardHtml() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="zh-CN">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>SkillFlow 效能监控看板 - 跨 Agent 技能遥测</title>
8
+ <style>
9
+ :root {
10
+ --bg: #000000;
11
+ --surface: rgba(28, 28, 30, 0.72);
12
+ --surface-elevated: rgba(44, 44, 46, 0.8);
13
+ --border: rgba(255, 255, 255, 0.08);
14
+ --border-highlight: rgba(255, 255, 255, 0.16);
15
+
16
+ --text-primary: #f5f5f7;
17
+ --text-secondary: #86868b;
18
+ --text-tertiary: #636366;
19
+
20
+ --apple-blue: #2997ff;
21
+ --apple-blue-glow: rgba(41, 151, 255, 0.25);
22
+ --apple-green: #30d158;
23
+ --apple-green-glow: rgba(48, 209, 88, 0.25);
24
+ --apple-purple: #bf5af2;
25
+ --apple-purple-glow: rgba(191, 90, 242, 0.25);
26
+ --apple-orange: #ff9f0a;
27
+ --apple-red: #ff453a;
28
+
29
+ --radius-sm: 10px;
30
+ --radius-md: 16px;
31
+ --radius-lg: 24px;
32
+ --radius-full: 9999px;
33
+
34
+ --blur: blur(32px) saturate(180%);
35
+ --shadow-card: 0 16px 36px -12px rgba(0, 0, 0, 0.65), 0 0 0 1px var(--border);
36
+ }
37
+
38
+ * { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
39
+
40
+ body {
41
+ background-color: var(--bg);
42
+ background-image:
43
+ radial-gradient(1200px circle at 50% -180px, rgba(41, 151, 255, 0.12), transparent 70%),
44
+ radial-gradient(800px circle at 90% 400px, rgba(191, 90, 242, 0.06), transparent 60%);
45
+ background-attachment: fixed;
46
+ color: var(--text-primary);
47
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif;
48
+ min-height: 100vh;
49
+ padding: 32px 24px 60px;
50
+ letter-spacing: -0.012em;
51
+ }
52
+
53
+ .container { max-width: 1240px; margin: 0 auto; }
54
+
55
+ header {
56
+ display: flex;
57
+ justify-content: space-between;
58
+ align-items: center;
59
+ padding: 14px 20px;
60
+ background: rgba(22, 22, 24, 0.65);
61
+ backdrop-filter: var(--blur);
62
+ -webkit-backdrop-filter: var(--blur);
63
+ border: 1px solid var(--border-highlight);
64
+ border-radius: var(--radius-lg);
65
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
66
+ margin-bottom: 32px;
67
+ flex-wrap: wrap;
68
+ gap: 16px;
69
+ }
70
+
71
+ .brand { display: flex; align-items: center; gap: 14px; }
72
+ .brand-icon {
73
+ width: 36px; height: 36px;
74
+ border-radius: var(--radius-sm);
75
+ background: linear-gradient(135deg, #2c2c2e, #1c1c1e);
76
+ border: 1px solid var(--border-highlight);
77
+ display: flex; align-items: center; justify-content: center;
78
+ box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.2);
79
+ }
80
+ .brand-icon svg { width: 20px; height: 20px; fill: var(--apple-blue); }
81
+ .brand-title { font-size: 19px; font-weight: 600; letter-spacing: -0.025em; color: #fff; }
82
+
83
+ .brand-badge {
84
+ display: inline-flex; align-items: center; gap: 6px;
85
+ font-size: 11px; color: var(--apple-green);
86
+ background: rgba(48, 209, 88, 0.12);
87
+ border: 1px solid rgba(48, 209, 88, 0.25);
88
+ padding: 3px 10px; border-radius: var(--radius-full); font-weight: 500;
89
+ }
90
+ .pulse-dot {
91
+ width: 6px; height: 6px; border-radius: 50%;
92
+ background: var(--apple-green);
93
+ box-shadow: 0 0 8px var(--apple-green);
94
+ animation: pulse 2s infinite ease-in-out;
95
+ }
96
+ @keyframes pulse {
97
+ 0%, 100% { opacity: 1; transform: scale(1); }
98
+ 50% { opacity: 0.4; transform: scale(0.85); }
99
+ }
100
+
101
+ .segmented-control {
102
+ display: inline-flex;
103
+ background: rgba(0, 0, 0, 0.45);
104
+ border: 1px solid var(--border);
105
+ padding: 3px; border-radius: var(--radius-full);
106
+ }
107
+ .segment-btn {
108
+ background: transparent; border: none;
109
+ color: var(--text-secondary); font-size: 13px; font-weight: 500;
110
+ padding: 6px 16px; border-radius: var(--radius-full);
111
+ cursor: pointer; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
112
+ }
113
+ .segment-btn:hover { color: var(--text-primary); }
114
+ .segment-btn.active {
115
+ background: rgba(255, 255, 255, 0.16);
116
+ color: #ffffff;
117
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.2);
118
+ }
119
+
120
+ .btn-refresh {
121
+ display: inline-flex; align-items: center; gap: 7px;
122
+ background: rgba(255, 255, 255, 0.08);
123
+ border: 1px solid var(--border-highlight);
124
+ color: var(--text-primary); font-size: 13px; font-weight: 500;
125
+ padding: 7px 15px; border-radius: var(--radius-full);
126
+ cursor: pointer; transition: all 0.2s ease;
127
+ }
128
+ .btn-refresh:hover { background: rgba(255, 255, 255, 0.14); transform: translateY(-1px); }
129
+ .btn-refresh svg { width: 13px; height: 13px; fill: currentColor; }
130
+
131
+ /* KPI Row */
132
+ .kpi-row {
133
+ display: grid;
134
+ grid-template-columns: repeat(4, 1fr);
135
+ gap: 18px;
136
+ margin-bottom: 28px;
137
+ }
138
+ @media (max-width: 1024px) { .kpi-row { grid-template-columns: repeat(2, 1fr); } }
139
+ @media (max-width: 600px) { .kpi-row { grid-template-columns: 1fr; } }
140
+
141
+ .card {
142
+ background: var(--surface);
143
+ backdrop-filter: var(--blur);
144
+ -webkit-backdrop-filter: var(--blur);
145
+ border: 1px solid var(--border);
146
+ border-radius: var(--radius-lg);
147
+ padding: 24px;
148
+ box-shadow: var(--shadow-card);
149
+ position: relative; overflow: hidden;
150
+ transition: transform 0.25s ease, border-color 0.25s ease;
151
+ }
152
+ .card:hover { border-color: var(--border-highlight); transform: translateY(-2px); }
153
+ .card::before {
154
+ content: ""; position: absolute;
155
+ top: 0; left: 0; right: 0; height: 1px;
156
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
157
+ }
158
+
159
+ .kpi-label {
160
+ font-size: 13px; color: var(--text-secondary);
161
+ font-weight: 500; margin-bottom: 12px;
162
+ display: flex; align-items: center; justify-content: space-between;
163
+ }
164
+ .kpi-number {
165
+ font-size: 38px; font-weight: 700;
166
+ letter-spacing: -0.035em; line-height: 1.05;
167
+ margin-bottom: 8px; font-variant-numeric: tabular-nums;
168
+ }
169
+ .kpi-meta {
170
+ font-size: 12px; color: var(--text-tertiary);
171
+ display: flex; align-items: center; gap: 6px;
172
+ }
173
+ .accent-green { color: var(--apple-green); }
174
+ .accent-blue { color: var(--apple-blue); }
175
+ .accent-purple { color: var(--apple-purple); }
176
+
177
+ /* Content Grid */
178
+ .dashboard-grid {
179
+ display: grid;
180
+ grid-template-columns: 1fr 1fr;
181
+ gap: 20px;
182
+ margin-bottom: 28px;
183
+ }
184
+ @media (max-width: 860px) { .dashboard-grid { grid-template-columns: 1fr; } }
185
+
186
+ .panel-header {
187
+ display: flex; justify-content: space-between; align-items: center;
188
+ margin-bottom: 20px; padding-bottom: 12px;
189
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
190
+ }
191
+ .panel-title {
192
+ font-size: 16px; font-weight: 600;
193
+ letter-spacing: -0.02em; display: flex; align-items: center; gap: 8px;
194
+ }
195
+ .panel-subtitle { font-size: 12px; color: var(--text-secondary); }
196
+
197
+ .bar-row { margin-bottom: 16px; }
198
+ .bar-label-group {
199
+ display: flex; justify-content: space-between;
200
+ font-size: 13px; margin-bottom: 7px;
201
+ }
202
+ .skill-rank-badge {
203
+ display: inline-flex; align-items: center; justify-content: center;
204
+ width: 18px; height: 18px; border-radius: 50%;
205
+ background: rgba(255, 255, 255, 0.1);
206
+ color: var(--text-secondary); font-size: 10px; font-weight: 600; margin-right: 8px;
207
+ }
208
+ .skill-title { font-weight: 500; color: #fff; }
209
+ .skill-stat { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
210
+
211
+ .progress-track {
212
+ background: rgba(255, 255, 255, 0.06);
213
+ height: 8px; border-radius: var(--radius-full);
214
+ overflow: hidden; width: 100%;
215
+ }
216
+ .progress-fill {
217
+ height: 100%; border-radius: var(--radius-full);
218
+ transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
219
+ }
220
+ .fill-gradient-mint {
221
+ background: linear-gradient(90deg, #30d158, #34c759);
222
+ box-shadow: 0 0 10px var(--apple-green-glow);
223
+ }
224
+ .fill-gradient-blue {
225
+ background: linear-gradient(90deg, #0071e3, #2997ff);
226
+ box-shadow: 0 0 10px var(--apple-blue-glow);
227
+ }
228
+
229
+ /* Table */
230
+ .table-card {
231
+ background: var(--surface);
232
+ backdrop-filter: var(--blur);
233
+ -webkit-backdrop-filter: var(--blur);
234
+ border: 1px solid var(--border);
235
+ border-radius: var(--radius-lg);
236
+ padding: 24px; box-shadow: var(--shadow-card);
237
+ overflow: hidden;
238
+ }
239
+ .table-responsive { overflow-x: auto; margin-top: 8px; }
240
+ table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
241
+ th {
242
+ text-align: left; font-weight: 500; color: var(--text-secondary);
243
+ padding: 12px 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.08);
244
+ font-size: 12px;
245
+ }
246
+ td {
247
+ padding: 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.04);
248
+ color: var(--text-primary); vertical-align: middle;
249
+ font-variant-numeric: tabular-nums;
250
+ }
251
+ tr:last-child td { border-bottom: none; }
252
+ tr:hover td { background: rgba(255, 255, 255, 0.03); }
253
+
254
+ .pill {
255
+ display: inline-flex; align-items: center; gap: 5px;
256
+ padding: 3px 10px; border-radius: var(--radius-full);
257
+ font-size: 11px; font-weight: 500;
258
+ }
259
+ .pill-agent {
260
+ background: rgba(41, 151, 255, 0.12); color: var(--apple-blue);
261
+ border: 1px solid rgba(41, 151, 255, 0.25);
262
+ }
263
+ .pill-model {
264
+ background: rgba(191, 90, 242, 0.12); color: var(--apple-purple);
265
+ border: 1px solid rgba(191, 90, 242, 0.25);
266
+ font-family: ui-monospace, monospace; font-size: 11px;
267
+ }
268
+ .pill-purple {
269
+ background: rgba(191, 90, 242, 0.12); color: var(--apple-purple);
270
+ border: 1px solid rgba(191, 90, 242, 0.25);
271
+ }
272
+ .pill-blue {
273
+ background: rgba(41, 151, 255, 0.12); color: var(--apple-blue);
274
+ border: 1px solid rgba(41, 151, 255, 0.25);
275
+ }
276
+ .pill-success {
277
+ background: rgba(48, 209, 88, 0.12); color: var(--apple-green);
278
+ border: 1px solid rgba(48, 209, 88, 0.25);
279
+ }
280
+ .pill-partial {
281
+ background: rgba(255, 159, 10, 0.12); color: var(--apple-orange);
282
+ border: 1px solid rgba(255, 159, 10, 0.25);
283
+ }
284
+ .pill-failed {
285
+ background: rgba(255, 69, 58, 0.12); color: var(--apple-red);
286
+ border: 1px solid rgba(255, 69, 58, 0.25);
287
+ }
288
+
289
+ footer {
290
+ text-align: center; margin-top: 48px;
291
+ font-size: 12px; color: var(--text-tertiary);
292
+ }
293
+ </style>
294
+ </head>
295
+ <body>
296
+ <div class="container">
297
+ <!-- Top Bar -->
298
+ <header>
299
+ <div class="brand">
300
+ <div class="brand-icon">
301
+ <svg viewBox="0 0 24 24">
302
+ <path d="M12 2L14.4 9.6L22 12L14.4 14.4L12 22L9.6 14.4L2 12L9.6 9.6L12 2Z"/>
303
+ </svg>
304
+ </div>
305
+ <div>
306
+ <div class="brand-title">SkillFlow</div>
307
+ </div>
308
+ <div class="brand-badge">
309
+ <span class="pulse-dot"></span>
310
+ 系统就绪
311
+ </div>
312
+ </div>
313
+
314
+ <div class="segmented-control">
315
+ <button class="segment-btn active" onclick="switchRange('all', this)">全部历史</button>
316
+ <button class="segment-btn" onclick="switchRange('today', this)">今日</button>
317
+ <button class="segment-btn" onclick="switchRange('7days', this)">近 7 天</button>
318
+ <button class="segment-btn" onclick="switchRange('30days', this)">近 30 天</button>
319
+ </div>
320
+
321
+ <div>
322
+ <button class="btn-refresh" onclick="fetchData()">
323
+ <svg viewBox="0 0 24 24">
324
+ <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
325
+ </svg>
326
+ 刷新
327
+ </button>
328
+ </div>
329
+ </header>
330
+
331
+ <!-- 4 Apple Pro Hero KPI Cards -->
332
+ <div class="kpi-row">
333
+ <div class="card">
334
+ <div class="kpi-label">
335
+ <span>技能触发总量</span>
336
+ <span style="font-size: 16px;">⚡</span>
337
+ </div>
338
+ <div class="kpi-number" id="kpiHits">-</div>
339
+ <div class="kpi-meta">执行成功率 <span class="accent-green" id="kpiSuccessRate">-</span></div>
340
+ </div>
341
+
342
+ <div class="card">
343
+ <div class="kpi-label">
344
+ <span>累计节省人工工时</span>
345
+ <span style="font-size: 16px;">⏱️</span>
346
+ </div>
347
+ <div class="kpi-number accent-purple" id="kpiHours">-</div>
348
+ <div class="kpi-meta">替代人工手动编写与排查</div>
349
+ </div>
350
+
351
+ <div class="card">
352
+ <div class="kpi-label">
353
+ <span>精简流程步骤</span>
354
+ <span style="font-size: 16px;">🌱</span>
355
+ </div>
356
+ <div class="kpi-number accent-green" id="kpiSteps">-</div>
357
+ <div class="kpi-meta">消除多轮重复交互</div>
358
+ </div>
359
+
360
+ <div class="card">
361
+ <div class="kpi-label">
362
+ <span>真实模型节约折算</span>
363
+ <span style="font-size: 16px;">🪙</span>
364
+ </div>
365
+ <div class="kpi-number accent-blue" id="kpiCost">-</div>
366
+ <div class="kpi-meta">节约 Token <span id="kpiTokens" style="color:#fff; font-weight:600;">-</span></div>
367
+ </div>
368
+ </div>
369
+
370
+ <!-- Charts Section -->
371
+ <div class="dashboard-grid">
372
+ <div class="card">
373
+ <div class="panel-header">
374
+ <div class="panel-title">
375
+ <span>🏆</span> 价值贡献最大 Skill 排行榜
376
+ </div>
377
+ <div class="panel-subtitle">按节省步骤与工时</div>
378
+ </div>
379
+ <div id="topSkillsList">加载中...</div>
380
+ </div>
381
+
382
+ <div class="card">
383
+ <div class="panel-header">
384
+ <div class="panel-title">
385
+ <span>🤖</span> 各 Agent 生态调用与节约贡献
386
+ </div>
387
+ <div class="panel-subtitle">跨平台分布</div>
388
+ </div>
389
+ <div id="agentBreakdownList">加载中...</div>
390
+ </div>
391
+ </div>
392
+
393
+ <!-- Live Invocations Table -->
394
+ <div class="table-card">
395
+ <div class="panel-header" style="margin-bottom: 8px;">
396
+ <div class="panel-title">
397
+ <span>⏱️</span> 实时 Skill 命中与模型明细
398
+ </div>
399
+ <div class="panel-subtitle" id="tableCount">展示最新调用流水</div>
400
+ </div>
401
+ <div class="table-responsive">
402
+ <table>
403
+ <thead>
404
+ <tr>
405
+ <th>时间</th>
406
+ <th>会话 ID</th>
407
+ <th>Agent 宿主</th>
408
+ <th>执行模型</th>
409
+ <th>命中的 Skill</th>
410
+ <th>价值模式</th>
411
+ <th>执行状态</th>
412
+ <th>节省工时</th>
413
+ <th>节省步骤</th>
414
+ <th>Token 收益 / 投入</th>
415
+ </tr>
416
+ </thead>
417
+ <tbody id="invocationsBody">
418
+ <tr><td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 28px 0;">正在连接本地数据库...</td></tr>
419
+ </tbody>
420
+ </table>
421
+ </div>
422
+ </div>
423
+
424
+ <footer>
425
+ SkillFlow MCP Telemetry Engine • Phase 1: Model-Aware Pricing & Work-Hour ROI<br>
426
+ 支持 Claude Code · Trae · Qoder · WorkBuddy · Codex · DSH
427
+ </footer>
428
+ </div>
429
+
430
+ <script>
431
+ let currentRange = 'all';
432
+
433
+ function switchRange(range, el) {
434
+ currentRange = range;
435
+ document.querySelectorAll('.segment-btn').forEach(btn => btn.classList.remove('active'));
436
+ el.classList.add('active');
437
+ fetchData();
438
+ }
439
+
440
+ async function fetchData() {
441
+ try {
442
+ const [dashRes, invRes] = await Promise.all([
443
+ fetch('/api/dashboard?time_range=' + currentRange),
444
+ fetch('/api/invocations?limit=30')
445
+ ]);
446
+ const dashData = await dashRes.json();
447
+ const invData = await invRes.json();
448
+
449
+ renderKpi(dashData.kpi);
450
+ renderTopSkills(dashData.skillRows || []);
451
+ renderAgentBreakdown(dashData.agentRows || [], dashData.kpi?.totalInvocations || 1);
452
+ renderInvocations(invData.invocations || []);
453
+ } catch (err) {
454
+ console.error('Fetch error:', err);
455
+ }
456
+ }
457
+
458
+ function renderKpi(kpi) {
459
+ if (!kpi) return;
460
+ document.getElementById('kpiHits').innerText = (kpi.totalInvocations || 0).toLocaleString();
461
+ document.getElementById('kpiSuccessRate').innerText = ((kpi.successRate || 1) * 100).toFixed(1) + '%';
462
+ document.getElementById('kpiSteps').innerText = '+' + (kpi.totalSavedSteps || 0).toLocaleString() + ' 步';
463
+ document.getElementById('kpiTokens').innerText = '+' + (kpi.totalSavedTokens || 0).toLocaleString();
464
+
465
+ const hours = kpi.totalSavedHours || 0;
466
+ document.getElementById('kpiHours').innerText = hours >= 1 ? '+' + hours.toFixed(1) + 'h' : '+' + Math.round(hours * 60) + 'm';
467
+
468
+ const cny = kpi.totalCostSavedCny || 0;
469
+ const usd = kpi.totalCostSavedUsd || 0;
470
+ document.getElementById('kpiCost').innerText = cny > 0 ? '¥' + cny.toFixed(2) : '$' + usd.toFixed(2);
471
+ }
472
+
473
+ function renderTopSkills(skills) {
474
+ const container = document.getElementById('topSkillsList');
475
+ if (!skills || skills.length === 0) {
476
+ container.innerHTML = '<div style="color: var(--text-secondary); padding: 32px 0; text-align: center;">暂无技能触发记录</div>';
477
+ return;
478
+ }
479
+ const maxSteps = Math.max(...skills.map(s => s.saved_steps), 1);
480
+ container.innerHTML = skills.slice(0, 5).map((s, idx) => \`
481
+ <div class="bar-row">
482
+ <div class="bar-label-group">
483
+ <div>
484
+ <span class="skill-rank-badge">\${idx + 1}</span>
485
+ <span class="skill-title">\${s.skill_name || s.skill_id}</span>
486
+ <span style="color: var(--text-tertiary); font-size: 11px; margin-left: 4px;">(\${s.skill_id})</span>
487
+ </div>
488
+ <div class="skill-stat">
489
+ <strong>+\${(s.saved_steps || 0).toLocaleString()} 步</strong>
490
+ <span style="color: var(--apple-purple); font-size: 11px; margin-left: 6px;">+\${(s.saved_hours || 0).toFixed(1)}h</span>
491
+ </div>
492
+ </div>
493
+ <div class="progress-track">
494
+ <div class="progress-fill fill-gradient-mint" style="width: \${Math.min(100, Math.max(6, Math.round(s.saved_steps / maxSteps * 100)))}%;"></div>
495
+ </div>
496
+ </div>
497
+ \`).join('');
498
+ }
499
+
500
+ function renderAgentBreakdown(agents, totalHits) {
501
+ const container = document.getElementById('agentBreakdownList');
502
+ if (!agents || agents.length === 0) {
503
+ container.innerHTML = '<div style="color: var(--text-secondary); padding: 32px 0; text-align: center;">暂无 Agent 接入数据</div>';
504
+ return;
505
+ }
506
+ container.innerHTML = agents.map(a => {
507
+ const pct = totalHits > 0 ? ((a.hit_cnt / totalHits) * 100).toFixed(1) : 0;
508
+ return \`
509
+ <div class="bar-row">
510
+ <div class="bar-label-group">
511
+ <span class="pill pill-agent">\${a.agent_name}</span>
512
+ <span class="skill-stat">\${a.hit_cnt} 次 (\${pct}%) | <span style="color: var(--apple-blue);">+\${(a.saved_tokens || 0).toLocaleString()} Tokens</span></span>
513
+ </div>
514
+ <div class="progress-track">
515
+ <div class="progress-fill fill-gradient-blue" style="width: \${Math.min(100, Math.max(6, pct))}%;"></div>
516
+ </div>
517
+ </div>
518
+ \`;
519
+ }).join('');
520
+ }
521
+
522
+ function escapeHtml(str) {
523
+ if (!str) return '';
524
+ return String(str)
525
+ .replace(/&/g, '&amp;')
526
+ .replace(/</g, '&lt;')
527
+ .replace(/>/g, '&gt;')
528
+ .replace(/"/g, '&quot;')
529
+ .replace(/'/g, '&#39;');
530
+ }
531
+
532
+ function renderInvocations(items) {
533
+ const tbody = document.getElementById('invocationsBody');
534
+ if (!items || items.length === 0) {
535
+ tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 32px 0;">暂无近期 Skill 调用流水</td></tr>';
536
+ return;
537
+ }
538
+ tbody.innerHTML = items.map(inv => {
539
+ const statusPill = inv.status === 'success'
540
+ ? '<span class="pill pill-success">成功</span>'
541
+ : inv.status === 'partial'
542
+ ? '<span class="pill pill-partial">部分</span>'
543
+ : '<span class="pill pill-failed">失败</span>';
544
+
545
+ const dateStr = inv.timestamp ? new Date(inv.timestamp).toLocaleTimeString() : '-';
546
+ const safeModel = escapeHtml(inv.model_name);
547
+ const modelBadge = safeModel && safeModel !== 'unknown'
548
+ ? \`<span class="pill pill-model">\${safeModel}</span>\`
549
+ : '<span style="color: var(--text-tertiary); font-size: 11px;">-</span>';
550
+
551
+ const hoursText = inv.saved_hours >= 1
552
+ ? \`+\${inv.saved_hours.toFixed(1)}h\`
553
+ : \`+\${Math.round((inv.saved_hours || 0) * 60)}m\`;
554
+
555
+ const profileBadge = inv.quality_profile === 'heavy_investment'
556
+ ? '<span class="pill pill-purple">品质重载</span>'
557
+ : inv.quality_profile === 'efficient'
558
+ ? '<span class="pill pill-success">高效精简</span>'
559
+ : '<span class="pill pill-blue">深度均衡</span>';
560
+
561
+ const tokenCell = (inv.net_tokens || 0) > 0
562
+ ? \`<span style="color: var(--apple-green); font-weight: 600;">+\${(inv.net_tokens || 0).toLocaleString()} Tokens</span>\`
563
+ : \`<span style="color: var(--apple-purple); font-size: 11px;">投入 \${(inv.actual_tokens || 0).toLocaleString()} Tokens</span>\`;
564
+
565
+ const safeSession = escapeHtml(inv.session_id);
566
+ const safeAgent = escapeHtml(inv.agent_name);
567
+ const safeSkill = escapeHtml(inv.skill_name || inv.skill_id);
568
+
569
+ return \`
570
+ <tr>
571
+ <td style="color: var(--text-secondary); font-size: 12px;">\${dateStr}</td>
572
+ <td style="font-family: ui-monospace, monospace; font-size: 11px; color: var(--text-secondary);">\${safeSession.substring(0, 16)}...</td>
573
+ <td><span class="pill pill-agent">\${safeAgent}</span></td>
574
+ <td>\${modelBadge}</td>
575
+ <td><strong>\${safeSkill}</strong></td>
576
+ <td>\${profileBadge}</td>
577
+ <td>\${statusPill}</td>
578
+ <td style="color: var(--apple-purple); font-weight: 500;">\${hoursText}</td>
579
+ <td style="color: var(--apple-green); font-weight: 600;">+\${inv.saved_steps} 步</td>
580
+ <td>\${tokenCell}</td>
581
+ </tr>
582
+ \`;
583
+ }).join('');
584
+ }
585
+
586
+ fetchData();
587
+ setInterval(fetchData, 5000);
588
+ </script>
589
+ </body>
590
+ </html>
591
+ `;
592
+ }
593
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/web/render.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6kBR,CAAC;AACF,CAAC"}
@@ -0,0 +1,10 @@
1
+ import http from 'node:http';
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ export interface WebServerInstance {
4
+ server: http.Server;
5
+ port: number;
6
+ url: string;
7
+ close: () => Promise<void>;
8
+ }
9
+ export declare function startWebServer(db: DatabaseSync, preferredPort?: number, maxAttempts?: number): Promise<WebServerInstance>;
10
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAgB,cAAc,CAC5B,EAAE,EAAE,YAAY,EAChB,aAAa,SAAO,EACpB,WAAW,SAAK,GACf,OAAO,CAAC,iBAAiB,CAAC,CAuI5B"}