openclaw-observability 2026.4.1 → 2026.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cloud/api-key-auth.d.ts.map +1 -1
- package/dist/cloud/api-key-auth.js +4 -9
- package/dist/cloud/api-key-auth.js.map +1 -1
- package/dist/cloud/types.d.ts +2 -3
- package/dist/cloud/types.d.ts.map +1 -1
- package/dist/config.d.ts +34 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -2
- package/dist/config.js.map +1 -1
- package/dist/gateway/register-observability-gateway.d.ts +6 -4
- package/dist/gateway/register-observability-gateway.d.ts.map +1 -1
- package/dist/gateway/register-observability-gateway.js +105 -2
- package/dist/gateway/register-observability-gateway.js.map +1 -1
- package/dist/hooks/messages.d.ts +4 -3
- package/dist/hooks/messages.d.ts.map +1 -1
- package/dist/hooks/messages.js +23 -1
- package/dist/hooks/messages.js.map +1 -1
- package/dist/hooks/session.d.ts +4 -3
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +9 -4
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/subagent.d.ts +4 -3
- package/dist/hooks/subagent.d.ts.map +1 -1
- package/dist/hooks/subagent.js +4 -1
- package/dist/hooks/subagent.js.map +1 -1
- package/dist/hooks/tools.d.ts +3 -3
- package/dist/hooks/tools.d.ts.map +1 -1
- package/dist/hooks/tools.js +122 -4
- package/dist/hooks/tools.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +472 -118
- package/dist/index.js.map +1 -1
- package/dist/llm/replay-runtime.d.ts +16 -0
- package/dist/llm/replay-runtime.d.ts.map +1 -0
- package/dist/llm/replay-runtime.js +596 -0
- package/dist/llm/replay-runtime.js.map +1 -0
- package/dist/llm/replay.d.ts +3 -0
- package/dist/llm/replay.d.ts.map +1 -1
- package/dist/llm/replay.js.map +1 -1
- package/dist/redaction.d.ts +1 -1
- package/dist/redaction.js +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/session-context.d.ts +4 -3
- package/dist/runtime/session-context.d.ts.map +1 -1
- package/dist/runtime/session-context.js +37 -17
- package/dist/runtime/session-context.js.map +1 -1
- package/dist/security/chain-detector.d.ts +4 -4
- package/dist/security/chain-detector.d.ts.map +1 -1
- package/dist/security/chain-detector.js.map +1 -1
- package/dist/security/rules.d.ts +2 -2
- package/dist/security/rules.d.ts.map +1 -1
- package/dist/security/rules.js +9 -2
- package/dist/security/rules.js.map +1 -1
- package/dist/security/scanner.d.ts +8 -3
- package/dist/security/scanner.d.ts.map +1 -1
- package/dist/security/scanner.js +85 -7
- package/dist/security/scanner.js.map +1 -1
- package/dist/security/types.d.ts +3 -0
- package/dist/security/types.d.ts.map +1 -1
- package/dist/storage/buffer.d.ts +7 -7
- package/dist/storage/buffer.d.ts.map +1 -1
- package/dist/storage/buffer.js +2 -2
- package/dist/storage/buffer.js.map +1 -1
- package/dist/storage/cloud-export-writer.d.ts +23 -0
- package/dist/storage/cloud-export-writer.d.ts.map +1 -0
- package/dist/storage/cloud-export-writer.js +202 -0
- package/dist/storage/cloud-export-writer.js.map +1 -0
- package/dist/storage/duckdb-local-writer.d.ts +19 -3
- package/dist/storage/duckdb-local-writer.d.ts.map +1 -1
- package/dist/storage/duckdb-local-writer.js +261 -81
- package/dist/storage/duckdb-local-writer.js.map +1 -1
- package/dist/storage/duckdb-observability-forwarder.d.ts +16 -0
- package/dist/storage/duckdb-observability-forwarder.d.ts.map +1 -0
- package/dist/storage/duckdb-observability-forwarder.js +289 -0
- package/dist/storage/duckdb-observability-forwarder.js.map +1 -0
- package/dist/storage/mysql-writer.d.ts +35 -6
- package/dist/storage/mysql-writer.d.ts.map +1 -1
- package/dist/storage/mysql-writer.js +251 -32
- package/dist/storage/mysql-writer.js.map +1 -1
- package/dist/storage/schema.d.ts +2 -2
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +181 -53
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/structured-model.d.ts +11 -2
- package/dist/storage/structured-model.d.ts.map +1 -1
- package/dist/storage/structured-model.js +183 -5
- package/dist/storage/structured-model.js.map +1 -1
- package/dist/storage/writer.d.ts +14 -2
- package/dist/storage/writer.d.ts.map +1 -1
- package/dist/types.d.ts +28 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/dist/types.js.map +1 -1
- package/dist/web/api.d.ts +80 -2
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +917 -113
- package/dist/web/api.js.map +1 -1
- package/dist/web/routes.d.ts +22 -2
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +264 -21
- package/dist/web/routes.js.map +1 -1
- package/dist/web/ui.d.ts +3 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +2678 -633
- package/dist/web/ui.js.map +1 -1
- package/openclaw.plugin.json +145 -4
- package/package.json +1 -1
package/dist/web/ui.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.getAppHtml = getAppHtml;
|
|
14
|
-
function getAppHtml() {
|
|
14
|
+
function getAppHtml(options) {
|
|
15
|
+
const showTenantScope = options?.showTenantScope !== false;
|
|
16
|
+
const uiBootstrap = `window.__OPENCLAW_OBS_UI=${JSON.stringify({ showTenantScope })};`;
|
|
15
17
|
return '<!DOCTYPE html>\n' +
|
|
16
18
|
'<html lang="en">\n' +
|
|
17
19
|
'<head>\n' +
|
|
@@ -26,6 +28,7 @@ function getAppHtml() {
|
|
|
26
28
|
'<body>\n' +
|
|
27
29
|
'<div id="app"></div>\n' +
|
|
28
30
|
'<script>\n' +
|
|
31
|
+
uiBootstrap + '\n' +
|
|
29
32
|
CLIENT_JS +
|
|
30
33
|
'</script>\n' +
|
|
31
34
|
'</body>\n' +
|
|
@@ -58,6 +61,7 @@ const CSS = `
|
|
|
58
61
|
--shadow-sm:0 1px 2px rgba(0,0,0,.2);
|
|
59
62
|
--shadow-md:0 4px 12px rgba(0,0,0,.25),0 0 0 1px rgba(255,255,255,.03);
|
|
60
63
|
--radius-sm:6px;--radius-md:8px;--radius-lg:12px;--radius-full:9999px;
|
|
64
|
+
--topbar-control-h:32px;
|
|
61
65
|
--duration-fast:.12s;--duration-normal:.2s;
|
|
62
66
|
--ease-out:cubic-bezier(.16,1,.3,1);
|
|
63
67
|
color-scheme:dark
|
|
@@ -89,25 +93,43 @@ a:hover{text-decoration:underline}
|
|
|
89
93
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:var(--radius-full)}
|
|
90
94
|
::-webkit-scrollbar-thumb:hover{background:var(--border-strong)}
|
|
91
95
|
|
|
92
|
-
/* ----
|
|
93
|
-
.shell{min-height:100vh;
|
|
94
|
-
.shell--trace{height:100vh;max-height:100vh;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
.
|
|
96
|
+
/* ---- App shell ---- */
|
|
97
|
+
.shell{min-height:100vh;background:var(--bg)}
|
|
98
|
+
.shell--trace{height:100vh;max-height:100vh;overflow:hidden}
|
|
99
|
+
.shell--trace .app-shell{height:100%;min-height:0}
|
|
100
|
+
.shell--trace .main-shell{height:100%;min-height:0}
|
|
101
|
+
.shell--trace .content--trace{height:100%}
|
|
102
|
+
.shell--trace .content--trace .content-inner{height:100%}
|
|
103
|
+
.app-shell{display:flex;min-height:100vh}
|
|
104
|
+
.side-nav{width:208px;flex:0 0 208px;border-right:1px solid var(--border);background:var(--bg-accent);display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
|
|
105
|
+
.side-nav-head{padding:14px 14px 10px;border-bottom:1px solid var(--border)}
|
|
99
106
|
.brand{display:flex;align-items:center;gap:10px}
|
|
100
107
|
.brand-logo{width:28px;height:28px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
|
101
108
|
.brand-logo svg{width:24px;height:24px;stroke:var(--accent);fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
102
109
|
.brand-text{display:flex;flex-direction:column;gap:1px}
|
|
103
110
|
.brand-title{font-size:16px;font-weight:700;letter-spacing:-.03em;line-height:1.1;color:var(--text-strong)}
|
|
104
111
|
.brand-sub{font-size:10px;font-weight:500;color:var(--muted);letter-spacing:.05em;text-transform:uppercase;line-height:1}
|
|
105
|
-
.
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
112
|
+
.side-nav-links{padding:10px;display:flex;flex-direction:column;gap:6px}
|
|
113
|
+
.side-nav-links a{color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:var(--radius-md);font-size:13px;font-weight:600;transition:all var(--duration-fast) var(--ease-out)}
|
|
114
|
+
.side-nav-links a:hover{color:var(--text);background:var(--bg-hover);text-decoration:none}
|
|
115
|
+
.side-nav-links a.active{color:var(--accent-foreground);background:var(--accent)}
|
|
116
|
+
.main-shell{min-width:0;flex:1;display:flex;flex-direction:column;min-height:100vh}
|
|
117
|
+
|
|
118
|
+
/* ---- Topbar ---- */
|
|
119
|
+
.topbar{position:sticky;top:0;z-index:40;display:flex;justify-content:space-between;align-items:center;gap:16px;padding:10px 20px;height:56px;border-bottom:1px solid var(--border);background:var(--bg)}
|
|
120
|
+
.topbar-left{display:flex;align-items:center;gap:10px;min-width:0;height:var(--topbar-control-h);flex-wrap:nowrap}
|
|
121
|
+
.tenant-field{display:flex;align-items:center;gap:8px;min-width:230px;height:var(--topbar-control-h);align-self:center}
|
|
122
|
+
.tenant-label{display:inline-flex;align-items:center;font-size:12px;line-height:1;color:var(--muted);font-weight:600;letter-spacing:.02em;text-transform:none;white-space:nowrap}
|
|
123
|
+
.tenant-dropdown{position:relative;min-width:140px;max-width:260px;flex:1}
|
|
124
|
+
.tenant-btn{width:100%;justify-content:space-between}
|
|
125
|
+
/* Topbar dropdowns: keep "main dropdown" look while enforcing aligned height */
|
|
126
|
+
.topbar .time-btn{height:var(--topbar-control-h);padding:0 12px;border-radius:10px;font-size:14px}
|
|
127
|
+
.tenant-menu{min-width:100%;max-height:320px;overflow:auto}
|
|
128
|
+
.tenant-menu .time-menu-item{padding:8px 12px}
|
|
129
|
+
.topbar-right{display:flex;align-items:center;gap:8px;height:var(--topbar-control-h)}
|
|
130
|
+
.topbar-right .tenant-dropdown{min-width:auto;max-width:none;flex:0 0 auto}
|
|
131
|
+
.build-id{font-family:var(--mono);font-size:10px;color:var(--muted);padding:2px 6px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated)}
|
|
132
|
+
.topbar-right .back-link{display:inline-flex;align-items:center;height:var(--topbar-control-h);gap:6px;color:var(--muted);font-size:13px;font-weight:500;padding:0 12px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);text-decoration:none;transition:all var(--duration-fast) var(--ease-out)}
|
|
111
133
|
.topbar-right .back-link:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover);text-decoration:none}
|
|
112
134
|
.topbar-right .back-link svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
113
135
|
.theme-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--muted);cursor:pointer;transition:all var(--duration-fast)}
|
|
@@ -130,10 +152,17 @@ a:hover{text-decoration:underline}
|
|
|
130
152
|
|
|
131
153
|
/* ---- Filter bar ---- */
|
|
132
154
|
.filter-bar{display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap}
|
|
133
|
-
.filter-bar input[type=text]{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:
|
|
155
|
+
.filter-bar input[type=text]{height:34px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:0 12px;font-size:13px;color:var(--text);outline:none;transition:border-color var(--duration-fast);flex:1;min-width:200px}
|
|
134
156
|
.filter-bar input[type=text]:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
157
|
+
.filter-bar .filter-select{height:32px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--text);font-size:13px;padding:0 30px 0 12px;outline:none;cursor:pointer;transition:all var(--duration-fast);appearance:none;-webkit-appearance:none;min-width:170px}
|
|
158
|
+
.filter-bar .time-btn{height:34px;padding:0 12px;border-radius:var(--radius-md);font-size:13px;gap:6px}
|
|
159
|
+
.filter-bar .icon-refresh-btn{height:34px;width:34px;min-width:34px}
|
|
160
|
+
.filter-bar .filter-select:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
161
|
+
.filter-bar .filter-select:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
162
|
+
.agent-type-dropdown{position:relative}
|
|
163
|
+
.agent-type-menu{min-width:140px}
|
|
135
164
|
.filter-bar .filter-sep{width:1px;height:24px;background:var(--border);flex-shrink:0}
|
|
136
|
-
.filter-bar .btn-clear{background:none;border:1px solid var(--border);border-radius:var(--radius-md);padding:
|
|
165
|
+
.filter-bar .btn-clear{height:34px;background:none;border:1px solid var(--border);border-radius:var(--radius-md);padding:0 12px;font-size:12px;color:var(--muted);cursor:pointer;transition:all var(--duration-fast)}
|
|
137
166
|
.filter-bar .btn-clear:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover)}
|
|
138
167
|
.sec-runtime{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px 18px;margin-bottom:14px;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
139
168
|
.sec-runtime-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}
|
|
@@ -176,8 +205,9 @@ a:hover{text-decoration:underline}
|
|
|
176
205
|
.sec-custom-form{display:grid;grid-template-columns:1fr 1.8fr auto;gap:8px;align-items:end}
|
|
177
206
|
.sec-custom-field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
178
207
|
.sec-custom-field-label{font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
179
|
-
.sec-custom-form input,.sec-custom-form select{height:
|
|
208
|
+
.sec-custom-form input,.sec-custom-form select{height:32px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--text);padding:0 12px;font-size:13px;outline:none;transition:all var(--duration-fast)}
|
|
180
209
|
.sec-custom-form input:focus,.sec-custom-form select:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
210
|
+
.sec-custom-form select:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
181
211
|
.sec-custom-form button{height:30px;border:1px solid var(--accent);border-radius:var(--radius-sm);background:var(--accent);color:var(--accent-foreground);padding:0 12px;font-size:12px;font-weight:700;cursor:pointer;letter-spacing:.01em}
|
|
182
212
|
.sec-custom-form button:hover{filter:brightness(1.06)}
|
|
183
213
|
.sec-custom-list{margin-top:10px;display:flex;flex-direction:column;gap:6px;max-height:220px;overflow:auto;padding-right:2px}
|
|
@@ -190,15 +220,19 @@ a:hover{text-decoration:underline}
|
|
|
190
220
|
.sec-custom-item-del:hover{background:rgba(239,68,68,.2);border-color:rgba(239,68,68,.55)}
|
|
191
221
|
/* Time range dropdown */
|
|
192
222
|
.time-dropdown{position:relative}
|
|
193
|
-
.time-btn{display:inline-flex;align-items:center;gap:6px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:
|
|
223
|
+
.time-btn{display:inline-flex;align-items:center;gap:6px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;padding:0 14px;height:42px;font-size:14px;color:var(--text);cursor:pointer;white-space:nowrap;transition:all var(--duration-fast)}
|
|
194
224
|
.time-btn:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
195
225
|
.time-btn svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5;flex-shrink:0}
|
|
196
|
-
.time-menu{position:absolute;top:100%;left:0;margin-top:
|
|
226
|
+
.time-menu{position:absolute;top:100%;left:0;margin-top:6px;background:var(--card);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-md);z-index:50;min-width:170px;padding:6px 0;display:none}
|
|
197
227
|
.time-menu.open{display:block}
|
|
198
|
-
.time-menu-item{padding:
|
|
228
|
+
.time-menu-item{padding:10px 16px;font-size:15px;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:8px;transition:background var(--duration-fast)}
|
|
199
229
|
.time-menu-item:hover{background:var(--bg-hover)}
|
|
200
230
|
.time-menu-item.active{color:var(--accent);font-weight:600}
|
|
201
231
|
.time-menu-item .check{width:14px;display:inline-block;text-align:center;font-size:12px}
|
|
232
|
+
.time-range-preview{display:inline-flex;align-items:center;height:42px;padding:0 14px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);font-family:var(--mono);font-size:13px;color:var(--muted);min-width:360px}
|
|
233
|
+
.time-input{height:42px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);color:var(--text);padding:0 12px;font-size:13px;outline:none;transition:all var(--duration-fast)}
|
|
234
|
+
.time-input:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
235
|
+
.time-input:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
202
236
|
|
|
203
237
|
/* ---- Section title ---- */
|
|
204
238
|
.section-title{font-size:15px;font-weight:600;color:var(--text-strong);margin-bottom:14px;display:flex;align-items:center;justify-content:space-between;letter-spacing:-.02em}
|
|
@@ -211,6 +245,8 @@ a:hover{text-decoration:underline}
|
|
|
211
245
|
.session-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
212
246
|
.session-id{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;background:var(--accent-subtle);padding:2px 8px;border-radius:var(--radius-sm)}
|
|
213
247
|
.session-model{font-size:12px;color:var(--muted);background:var(--secondary);padding:2px 8px;border-radius:var(--radius-sm);border:1px solid var(--border)}
|
|
248
|
+
.session-role{font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px;border:1px solid transparent;letter-spacing:.02em}
|
|
249
|
+
.session-role.subagent{color:#1d4ed8;background:#eff6ff;border-color:#bfdbfe}
|
|
214
250
|
.session-user{font-size:12px;color:var(--muted)}
|
|
215
251
|
.session-channel{font-size:12px;color:var(--muted);background:var(--secondary);padding:2px 8px;border-radius:var(--radius-sm);border:1px solid var(--border)}
|
|
216
252
|
.session-time{font-size:11px;color:var(--muted);margin-left:auto;font-family:var(--mono)}
|
|
@@ -220,6 +256,10 @@ a:hover{text-decoration:underline}
|
|
|
220
256
|
.mini-trace{display:flex;gap:3px;flex-wrap:wrap;margin-left:auto}
|
|
221
257
|
.mini-dot{width:18px;height:6px;border-radius:3px;opacity:.75}
|
|
222
258
|
.mini-dot-more{font-size:10px;color:var(--muted);margin-left:2px;white-space:nowrap}
|
|
259
|
+
.skills-list{gap:10px}
|
|
260
|
+
.skills-card{cursor:pointer}
|
|
261
|
+
.skills-card.is-active{border-color:var(--accent);box-shadow:var(--shadow-md),inset 0 1px 0 var(--card-highlight)}
|
|
262
|
+
.skills-expand{display:none;margin-top:12px;padding-top:12px;border-top:1px dashed var(--border)}
|
|
223
263
|
|
|
224
264
|
/* ---- Pagination (matches OpenClaw .btn) ---- */
|
|
225
265
|
.pagination{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:20px;padding:12px 0}
|
|
@@ -232,9 +272,17 @@ a:hover{text-decoration:underline}
|
|
|
232
272
|
.content--trace{padding:0!important;overflow:hidden!important;flex:1!important;min-height:0!important;display:flex!important;flex-direction:column!important}
|
|
233
273
|
.content--trace .content-inner{max-width:none;width:100%;margin:0;flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}
|
|
234
274
|
|
|
235
|
-
.trace-top{flex:1;min-width:0;min-height:120px;overflow-y:
|
|
275
|
+
.trace-top{flex:1;min-width:0;min-height:120px;overflow-y:auto;scrollbar-gutter:stable;position:relative}
|
|
236
276
|
.trace-header{position:sticky;top:0;z-index:12;background:var(--card);padding:16px 24px;border-bottom:1px solid var(--border);box-shadow:inset 0 1px 0 var(--card-highlight),0 6px 18px rgba(0,0,0,.08)}
|
|
237
277
|
.trace-header-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
278
|
+
.trace-header-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
279
|
+
.trace-run-state{font-size:11px;font-weight:700;padding:3px 10px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;border:1px solid transparent}
|
|
280
|
+
.trace-run-state.completed{color:var(--ok);background:var(--ok-subtle);border-color:rgba(34,197,94,.3)}
|
|
281
|
+
.trace-run-state.failed{color:var(--danger);background:var(--danger-subtle);border-color:rgba(239,68,68,.3)}
|
|
282
|
+
.trace-run-state.running{color:var(--warn);background:var(--warn-subtle);border-color:rgba(245,158,11,.3)}
|
|
283
|
+
.trace-replay-btn{height:26px;display:inline-flex;align-items:center;justify-content:center;padding:0 10px;border:1px solid var(--border);border-radius:var(--radius-full);background:var(--bg-elevated);color:var(--text);font-size:11px;font-weight:600;cursor:pointer;transition:all var(--duration-fast)}
|
|
284
|
+
.trace-replay-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent);background:var(--accent-subtle)}
|
|
285
|
+
.trace-replay-btn:disabled{opacity:.45;cursor:not-allowed}
|
|
238
286
|
.trace-back{display:inline-flex;align-items:center;gap:6px;color:var(--accent);text-decoration:none;font-size:13px;margin-bottom:10px;font-weight:500}
|
|
239
287
|
.trace-header-top .trace-back{margin-bottom:0}
|
|
240
288
|
.trace-back:hover{text-decoration:underline}
|
|
@@ -242,6 +290,17 @@ a:hover{text-decoration:underline}
|
|
|
242
290
|
.trace-meta{display:flex;gap:16px;flex-wrap:wrap}
|
|
243
291
|
.trace-meta-item{font-size:13px;color:var(--muted)}
|
|
244
292
|
.trace-meta-item b{color:var(--text-strong);font-weight:600}
|
|
293
|
+
.trace-subagents{margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px}
|
|
294
|
+
.trace-subagent-item{border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);padding:8px 10px}
|
|
295
|
+
.trace-subagent-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
296
|
+
.trace-subagent-name{font-size:12px;font-weight:700;color:var(--text-strong)}
|
|
297
|
+
.trace-subagent-status{font-size:10px;font-weight:700;padding:2px 8px;border-radius:var(--radius-full);text-transform:uppercase;letter-spacing:.04em}
|
|
298
|
+
.trace-subagent-status.running{color:var(--warn);background:var(--warn-subtle);border:1px solid rgba(245,158,11,.35)}
|
|
299
|
+
.trace-subagent-status.done{color:var(--ok);background:var(--ok-subtle);border:1px solid rgba(34,197,94,.35)}
|
|
300
|
+
.trace-subagent-status.fail{color:var(--danger);background:var(--danger-subtle);border:1px solid rgba(239,68,68,.35)}
|
|
301
|
+
.trace-subagent-meta{margin-top:5px;font-size:11px;color:var(--muted);line-height:1.4}
|
|
302
|
+
.trace-subagent-link{font-family:var(--mono);font-size:11px}
|
|
303
|
+
.trace-subagent-link a{color:var(--accent)}
|
|
245
304
|
.trace-search-bar{margin-top:14px;margin-bottom:0}
|
|
246
305
|
.trace-search-bar input[type=text]{min-width:280px}
|
|
247
306
|
.trace-search-stats{font-size:12px;color:var(--muted);white-space:nowrap}
|
|
@@ -284,6 +343,10 @@ a:hover{text-decoration:underline}
|
|
|
284
343
|
.wf-group-title{font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-strong)}
|
|
285
344
|
.wf-group-hit{font-size:10px;padding:1px 6px;border-radius:var(--radius-full);background:var(--accent-subtle);color:var(--accent);font-weight:700;flex-shrink:0}
|
|
286
345
|
.wf-group-snippet{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
|
346
|
+
.wf-group-right{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
347
|
+
.wf-group-replay-btn{height:24px;display:inline-flex;align-items:center;justify-content:center;padding:0 8px;border:1px solid var(--border);border-radius:var(--radius-full);background:var(--bg-elevated);color:var(--text);font-size:11px;font-weight:600;cursor:pointer;transition:all var(--duration-fast)}
|
|
348
|
+
.wf-group-replay-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent);background:var(--accent-subtle)}
|
|
349
|
+
.wf-group-replay-btn:disabled{opacity:.45;cursor:not-allowed}
|
|
287
350
|
.wf-group-meta{font-size:11px;color:var(--muted);font-family:var(--mono);background:var(--secondary);border:1px solid var(--border);border-radius:var(--radius-full);padding:3px 10px;flex-shrink:0}
|
|
288
351
|
.search-hit{background:rgba(245,158,11,.22);color:var(--text-strong);border-radius:3px;padding:0 2px}
|
|
289
352
|
|
|
@@ -297,7 +360,7 @@ body.resizing{cursor:col-resize!important;-webkit-user-select:none!important;use
|
|
|
297
360
|
body.resizing *{cursor:col-resize!important}
|
|
298
361
|
|
|
299
362
|
/* ---- Detail panel (right pane) ---- */
|
|
300
|
-
.trace-bottom{flex:0 0 420px;min-width:320px;max-width:720px;overflow-y:
|
|
363
|
+
.trace-bottom{flex:0 0 420px;min-width:320px;max-width:720px;overflow-y:auto;scrollbar-gutter:stable;background:var(--bg-accent);border-left:1px solid var(--border)}
|
|
301
364
|
.trace-bottom .detail-panel{background:var(--card);overflow:hidden;border:none;border-radius:0;margin:0;width:100%}
|
|
302
365
|
.trace-bottom .detail-header{padding:10px 18px;background:var(--bg-accent);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;position:sticky;top:0;z-index:5}
|
|
303
366
|
.detail-header .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
|
@@ -397,8 +460,27 @@ body.resizing *{cursor:col-resize!important}
|
|
|
397
460
|
.an-grid{display:grid;gap:20px;margin-bottom:24px}
|
|
398
461
|
.an-grid-2{grid-template-columns:1fr 1fr}
|
|
399
462
|
.an-grid-3{grid-template-columns:1fr 1fr 1fr}
|
|
400
|
-
.metrics-kpi-grid{display:grid;gap:14px;grid-template-columns:repeat(
|
|
401
|
-
|
|
463
|
+
.metrics-kpi-grid{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));margin-bottom:16px}
|
|
464
|
+
.metrics-panel-grid{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));margin-bottom:16px}
|
|
465
|
+
.metrics-series-grid{display:grid;gap:14px;grid-template-columns:repeat(2,minmax(0,1fr));margin-bottom:16px}
|
|
466
|
+
.metrics-panel{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--card);padding:14px 16px;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
467
|
+
.metrics-panel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
468
|
+
.metrics-panel-title{font-size:16px;font-weight:700;color:var(--text-strong);letter-spacing:-.02em}
|
|
469
|
+
.metrics-panel-sub{font-size:11px;color:var(--muted)}
|
|
470
|
+
.metrics-panel-chart{position:relative;height:220px;border:1px solid var(--border);border-radius:var(--radius-md);padding:8px;background:var(--bg-elevated)}
|
|
471
|
+
.metrics-panel-svg{width:100%;height:100%;display:block}
|
|
472
|
+
.metrics-panel-legend{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px}
|
|
473
|
+
.metrics-panel-legend-item{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
474
|
+
.metrics-panel-dot{width:9px;height:9px;border-radius:999px}
|
|
475
|
+
.metrics-panel-overlay{position:absolute;left:8px;right:8px;top:8px;bottom:8px;z-index:2;cursor:crosshair;touch-action:none;-webkit-user-select:none;user-select:none}
|
|
476
|
+
.metrics-panel-marker{position:absolute;top:8px;bottom:8px;width:1px;background:var(--accent);opacity:.75;display:none;z-index:3;pointer-events:none}
|
|
477
|
+
.metrics-panel-point{position:absolute;width:8px;height:8px;border-radius:999px;border:2px solid var(--bg-elevated);display:none;z-index:4;pointer-events:none;transform:translate(-50%,-50%)}
|
|
478
|
+
.metrics-panel-tip{position:absolute;z-index:5;top:12px;left:12px;max-width:320px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 8px;font-size:11px;line-height:1.35;color:var(--text);box-shadow:var(--shadow-md);pointer-events:none;display:none}
|
|
479
|
+
@media(max-width:900px){
|
|
480
|
+
.an-grid-2,.an-grid-3{grid-template-columns:1fr}
|
|
481
|
+
.metrics-kpi-grid,.metrics-panel-grid,.metrics-series-grid{grid-template-columns:1fr}
|
|
482
|
+
.time-range-preview{min-width:0;flex:1;width:100%}
|
|
483
|
+
}
|
|
402
484
|
.an-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
403
485
|
.an-card h3{font-size:14px;font-weight:600;color:var(--text-strong);margin-bottom:16px;display:flex;align-items:center;gap:8px;letter-spacing:-.02em}
|
|
404
486
|
.an-card h3 .icon{font-size:16px}
|
|
@@ -432,9 +514,45 @@ body.resizing *{cursor:col-resize!important}
|
|
|
432
514
|
.metrics-toolbar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;margin-bottom:16px}
|
|
433
515
|
.metrics-toolbar .toolbar-field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
434
516
|
.metrics-toolbar .toolbar-label{font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
|
|
435
|
-
.metrics-select{height:
|
|
517
|
+
.metrics-select{height:42px;padding:0 30px 0 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);color:var(--text);font-size:13px;outline:none;transition:all var(--duration-fast);appearance:none;-webkit-appearance:none}
|
|
518
|
+
.metrics-select:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
436
519
|
.metrics-select:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
437
520
|
.metrics-select--grow{min-width:200px;flex:1;max-width:560px}
|
|
521
|
+
.metrics-toolbar .icon-refresh-btn{width:42px;height:42px;border-radius:10px}
|
|
522
|
+
.btn-apply{width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);color:var(--text);cursor:pointer;transition:all var(--duration-fast);padding:0;flex-shrink:0}
|
|
523
|
+
.btn-apply:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
524
|
+
.btn-apply svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
525
|
+
.replay-toolbar{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:14px}
|
|
526
|
+
.replay-toolbar .toolbar-field{display:flex;flex-direction:column;gap:6px}
|
|
527
|
+
.replay-toolbar .toolbar-label{font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
|
|
528
|
+
.replay-input{height:42px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);color:var(--text);padding:0 12px;font-size:14px;outline:none;transition:all var(--duration-fast)}
|
|
529
|
+
.replay-input:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
530
|
+
.replay-input:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
531
|
+
.replay-area{min-height:112px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);color:var(--text);padding:10px 12px;font-size:14px;line-height:1.55;resize:vertical;outline:none;transition:all var(--duration-fast)}
|
|
532
|
+
.replay-area:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
533
|
+
.replay-area:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
534
|
+
.replay-actions{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
535
|
+
.replay-run{height:42px;display:inline-flex;align-items:center;justify-content:center;padding:0 16px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);color:var(--text);font-size:14px;font-weight:600;cursor:pointer;transition:all var(--duration-fast)}
|
|
536
|
+
.replay-run:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
537
|
+
.replay-run:disabled{opacity:.5;cursor:not-allowed}
|
|
538
|
+
.replay-meta{font-size:12px;color:var(--muted)}
|
|
539
|
+
.replay-result{border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);padding:12px;min-height:140px;white-space:pre-wrap;line-height:1.6;font-size:14px}
|
|
540
|
+
.replay-compare{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
541
|
+
.replay-compare .replay-result{min-height:180px}
|
|
542
|
+
.replay-caption{font-size:12px;color:var(--muted);margin:6px 0}
|
|
543
|
+
.replay-kv-list{display:flex;flex-direction:column;gap:10px;margin-top:8px}
|
|
544
|
+
.replay-kv-item{border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated);padding:10px 12px}
|
|
545
|
+
.replay-kv-key{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--info);margin-bottom:8px;word-break:break-all}
|
|
546
|
+
.replay-kv-primitive{width:100%;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);padding:8px 10px;font-size:13px;line-height:1.5;outline:none}
|
|
547
|
+
.replay-kv-primitive:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
548
|
+
.replay-kv-json{width:100%;min-height:120px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);padding:8px 10px;font-size:12px;line-height:1.5;outline:none;resize:vertical;font-family:var(--mono)}
|
|
549
|
+
.replay-kv-json:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
550
|
+
.replay-kv-summary{cursor:pointer;font-size:12px;color:var(--muted);user-select:none}
|
|
551
|
+
.replay-kv-summary:hover{color:var(--text)}
|
|
552
|
+
.replay-raw-details{margin-top:8px}
|
|
553
|
+
.replay-raw-details summary{cursor:pointer;font-size:12px;color:var(--muted);user-select:none}
|
|
554
|
+
.replay-raw-details summary:hover{color:var(--text)}
|
|
555
|
+
@media(max-width:900px){.replay-compare{grid-template-columns:1fr}}
|
|
438
556
|
.metrics-callout{border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px 20px;background:var(--card);margin-bottom:16px;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
439
557
|
.metrics-callout-title{font-size:15px;font-weight:700;color:var(--text-strong);letter-spacing:-.02em}
|
|
440
558
|
.metrics-callout-body{font-size:13px;color:var(--text);margin-top:10px;line-height:1.55}
|
|
@@ -501,6 +619,15 @@ body.resizing *{cursor:col-resize!important}
|
|
|
501
619
|
/* ---- Responsive ---- */
|
|
502
620
|
@media(max-width:900px){.stat-grid{grid-template-columns:repeat(3,1fr)}}
|
|
503
621
|
@media(max-width:900px){
|
|
622
|
+
.app-shell{display:block}
|
|
623
|
+
.side-nav{width:100%;height:auto;position:static;border-right:none;border-bottom:1px solid var(--border)}
|
|
624
|
+
.side-nav-links{flex-direction:row;flex-wrap:wrap}
|
|
625
|
+
.main-shell{min-height:0}
|
|
626
|
+
.topbar{height:auto;min-height:56px;padding:10px 12px;flex-wrap:wrap}
|
|
627
|
+
.topbar-left{width:100%;height:auto;flex-wrap:wrap;align-items:center}
|
|
628
|
+
.tenant-field{min-width:130px;flex:1;max-width:none}
|
|
629
|
+
.tenant-dropdown{min-width:100px;max-width:none;flex:1}
|
|
630
|
+
.topbar-right{height:auto}
|
|
504
631
|
.content--trace .content-inner{flex-direction:column}
|
|
505
632
|
.trace-resize{display:none}
|
|
506
633
|
.trace-bottom{flex:0 0 320px;min-width:0;max-width:none;border-left:none;border-top:1px solid var(--border)}
|
|
@@ -509,7 +636,7 @@ body.resizing *{cursor:col-resize!important}
|
|
|
509
636
|
}
|
|
510
637
|
@media(max-width:560px){
|
|
511
638
|
.stat-grid{grid-template-columns:repeat(2,1fr)}
|
|
512
|
-
.topbar{padding:
|
|
639
|
+
.topbar{padding:8px 10px}
|
|
513
640
|
.content{padding:12px}
|
|
514
641
|
.waterfall{margin:10px}
|
|
515
642
|
.wf-header,.wf-row{grid-template-columns:200px 70px 1fr}
|
|
@@ -520,6 +647,7 @@ body.resizing *{cursor:col-resize!important}
|
|
|
520
647
|
/* ================================================================== */
|
|
521
648
|
const CLIENT_JS = `
|
|
522
649
|
"use strict";
|
|
650
|
+
var UI_BUILD_ID = "obs-ui-20260403-1435";
|
|
523
651
|
|
|
524
652
|
// Use localhost secure-context semantics for control-ui websocket auth.
|
|
525
653
|
// 127.0.0.1 is not always treated the same as localhost by gateway policy.
|
|
@@ -534,7 +662,7 @@ try {
|
|
|
534
662
|
/* ---------- constants ---------- */
|
|
535
663
|
var API = window.location.pathname.replace(/\\/+$/, '') + '/api';
|
|
536
664
|
var TYPE_COLORS = {
|
|
537
|
-
message:'#8b5cf6', assistant_stream:'#7c3aed', thinking:'#0f766e', tool_call:'#f59e0b', tool_update:'#fb923c', tool_persist:'#f97316',
|
|
665
|
+
message:'#8b5cf6', replay:'#0891b2', assistant_stream:'#7c3aed', thinking:'#0f766e', tool_call:'#f59e0b', tool_update:'#fb923c', tool_persist:'#f97316',
|
|
538
666
|
prompt_build:'#3b82f6', model_resolve:'#06b6d4', agent_end:'#10b981',
|
|
539
667
|
session_start:'#22c55e', session_end:'#ef4444', session_snapshot:'#16a34a', compaction:'#14b8a6',
|
|
540
668
|
reset:'#f43f5e', user_message:'#0ea5e9', assistant_msg:'#a855f7',
|
|
@@ -542,7 +670,7 @@ var TYPE_COLORS = {
|
|
|
542
670
|
gateway_start:'#94a3b8', gateway_stop:'#475569'
|
|
543
671
|
};
|
|
544
672
|
var TYPE_LABELS = {
|
|
545
|
-
message:'LLM Call', assistant_stream:'Assistant Stream', thinking:'Thinking', tool_call:'Tool Call', tool_update:'Tool Update', tool_persist:'Tool Persist',
|
|
673
|
+
message:'LLM Call', replay:'Replay', assistant_stream:'Assistant Stream', thinking:'Thinking', tool_call:'Tool Call', tool_update:'Tool Update', tool_persist:'Tool Persist',
|
|
546
674
|
prompt_build:'Prompt Build', model_resolve:'Model Resolve', agent_end:'Agent End',
|
|
547
675
|
session_start:'Session Start', session_end:'Session End', session_snapshot:'Session Snapshot', compaction:'Compaction',
|
|
548
676
|
reset:'Reset', user_message:'User Message', assistant_msg:'Assistant Msg',
|
|
@@ -554,28 +682,293 @@ var app = document.getElementById('app');
|
|
|
554
682
|
var currentPage = 1;
|
|
555
683
|
var filterSearch = '';
|
|
556
684
|
var filterTimeRange = '24h'; // default to 24h for better performance
|
|
685
|
+
var filterTimeFrom = '';
|
|
686
|
+
var filterTimeTo = '';
|
|
687
|
+
var filterAgentType = 'main'; // default hide subagent in list
|
|
688
|
+
var tenantOptions = [];
|
|
689
|
+
var selectedScopeId = 'local';
|
|
690
|
+
var defaultScopeId = 'local';
|
|
691
|
+
var tenantsLoaded = false;
|
|
692
|
+
var tenantsLoading = null;
|
|
693
|
+
var SHOW_TENANT_SCOPE = !(window.__OPENCLAW_OBS_UI && window.__OPENCLAW_OBS_UI.showTenantScope === false);
|
|
694
|
+
var SCOPE_STORAGE_KEY = 'openclaw.observability.scopeId';
|
|
557
695
|
var metricsRangeMinutes = 60;
|
|
558
696
|
var metricsStepSec = 30;
|
|
697
|
+
var metricsTimeFrom = '';
|
|
698
|
+
var metricsTimeTo = '';
|
|
559
699
|
var metricsTableSort = 'name';
|
|
560
700
|
var metricsSnapshotFilter = '';
|
|
561
701
|
var metricsSelected = '';
|
|
562
702
|
/** Set true to show the Metrics nav item and #/metrics page (hidden by default). */
|
|
563
|
-
var SHOW_METRICS_PAGE =
|
|
703
|
+
var SHOW_METRICS_PAGE = true;
|
|
564
704
|
var sparklineSeq = 0;
|
|
565
705
|
var sparklineStore = {};
|
|
706
|
+
var metricsPanelSeq = 0;
|
|
707
|
+
var metricsPanelStore = {};
|
|
708
|
+
var METRICS_WINDOW_OPTIONS = [30, 60, 180, 360, 720, 1440];
|
|
709
|
+
var replayProvidersCache = [];
|
|
710
|
+
var replayState = {
|
|
711
|
+
providerId: '',
|
|
712
|
+
model: '',
|
|
713
|
+
systemPrompt: '',
|
|
714
|
+
userPrompt: '',
|
|
715
|
+
replayInput: null,
|
|
716
|
+
replayInputText: '',
|
|
717
|
+
replayInputError: '',
|
|
718
|
+
temperature: '',
|
|
719
|
+
maxTokens: '',
|
|
720
|
+
running: false,
|
|
721
|
+
result: null,
|
|
722
|
+
error: '',
|
|
723
|
+
sourceSessionId: '',
|
|
724
|
+
sourceObservationId: '',
|
|
725
|
+
sourceActionName: '',
|
|
726
|
+
sourceCreatedAt: '',
|
|
727
|
+
baselineText: '',
|
|
728
|
+
baselinePromptTokens: null,
|
|
729
|
+
baselineCompletionTokens: null,
|
|
730
|
+
baselineLatencyMs: null,
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
var LANG_STORAGE_KEY = 'openclaw.observability.lang';
|
|
734
|
+
function normalizeLang(v) {
|
|
735
|
+
var s = String(v || '').trim().toLowerCase();
|
|
736
|
+
return s === 'zh' ? 'zh' : 'en';
|
|
737
|
+
}
|
|
738
|
+
function loadLang() {
|
|
739
|
+
try {
|
|
740
|
+
return normalizeLang(window.localStorage.getItem(LANG_STORAGE_KEY) || '');
|
|
741
|
+
} catch (_) {
|
|
742
|
+
return 'en';
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
var currentLang = loadLang();
|
|
746
|
+
var I18N = {
|
|
747
|
+
en: {
|
|
748
|
+
nav_dashboard: 'Dashboard',
|
|
749
|
+
nav_trace: 'Trace',
|
|
750
|
+
nav_skills: 'Skills',
|
|
751
|
+
nav_replay: 'Replay',
|
|
752
|
+
nav_metrics: 'Metrics',
|
|
753
|
+
nav_security: 'Security',
|
|
754
|
+
top_control_panel: 'Control Panel',
|
|
755
|
+
lang: 'Language',
|
|
756
|
+
tenant_scope: 'Scope ID',
|
|
757
|
+
select_scope: 'Select scope',
|
|
758
|
+
loading: 'Loading...',
|
|
759
|
+
loading_dashboard: 'Loading dashboard...',
|
|
760
|
+
loading_skills: 'Loading skills...',
|
|
761
|
+
loading_metrics: 'Loading metrics...',
|
|
762
|
+
loading_replay: 'Loading replay workspace...',
|
|
763
|
+
failed_load: 'Failed to load: ',
|
|
764
|
+
sessions: 'Sessions',
|
|
765
|
+
actions: 'Actions',
|
|
766
|
+
tokens: 'Tokens',
|
|
767
|
+
avg_latency: 'Avg Latency',
|
|
768
|
+
success: 'Success',
|
|
769
|
+
search_session: 'Search session ID, key, user, model...',
|
|
770
|
+
search: 'Search',
|
|
771
|
+
clear: 'Clear',
|
|
772
|
+
refresh_trace_list: 'Refresh trace list',
|
|
773
|
+
traces: 'Traces',
|
|
774
|
+
matched: 'matched',
|
|
775
|
+
total: 'total',
|
|
776
|
+
no_sessions: 'No sessions recorded yet',
|
|
777
|
+
main: 'Main',
|
|
778
|
+
subagent: 'Subagent',
|
|
779
|
+
system: 'System',
|
|
780
|
+
replay: 'Replay',
|
|
781
|
+
all: 'All',
|
|
782
|
+
filter_agent_type: 'Filter agent type',
|
|
783
|
+
start_time: 'Start time',
|
|
784
|
+
end_time: 'End time',
|
|
785
|
+
page: 'Page',
|
|
786
|
+
prev: 'Prev',
|
|
787
|
+
next: 'Next',
|
|
788
|
+
parent: 'Parent',
|
|
789
|
+
heuristic_subagent_session: 'Heuristic subagent session',
|
|
790
|
+
actions_unit: 'actions',
|
|
791
|
+
tokens_unit: 'tokens',
|
|
792
|
+
trace_detail_loading: 'Loading trace...',
|
|
793
|
+
back_to_traces: 'Back to Traces',
|
|
794
|
+
completed: 'Completed',
|
|
795
|
+
failed: 'Failed',
|
|
796
|
+
running: 'Running',
|
|
797
|
+
duration: 'Duration',
|
|
798
|
+
snapshots: 'Snapshots',
|
|
799
|
+
time: 'Time',
|
|
800
|
+
search_this_session: 'Search this session: action, input, output, model...',
|
|
801
|
+
refresh_current_trace: 'Refresh current trace',
|
|
802
|
+
prev: 'Prev',
|
|
803
|
+
next: 'Next',
|
|
804
|
+
search_within_session: 'Search within this session',
|
|
805
|
+
no_action_matched: 'No action matched',
|
|
806
|
+
try_hint: 'Try a tool name, model, prompt fragment, or error text.',
|
|
807
|
+
input: 'Input',
|
|
808
|
+
output: 'Output',
|
|
809
|
+
type: 'Type',
|
|
810
|
+
model: 'Model',
|
|
811
|
+
agent: 'Agent',
|
|
812
|
+
channel: 'Channel',
|
|
813
|
+
prompt_tokens: 'Prompt Tokens',
|
|
814
|
+
completion_tokens: 'Completion Tokens',
|
|
815
|
+
metrics_disabled: 'Disabled',
|
|
816
|
+
refresh_metrics: 'Refresh metrics',
|
|
817
|
+
apply_time_range: 'Apply time range',
|
|
818
|
+
start: 'Start',
|
|
819
|
+
end: 'End',
|
|
820
|
+
latest_metrics_snapshot: 'Latest Metrics Snapshot',
|
|
821
|
+
filter_by_metric_name: 'Filter by metric name…',
|
|
822
|
+
no_replay_result: 'No replay result yet.',
|
|
823
|
+
run_replay: 'Run Replay',
|
|
824
|
+
replay_running: 'Running...',
|
|
825
|
+
replay_title: 'Replay',
|
|
826
|
+
replay_subtitle: 'LLM sandbox',
|
|
827
|
+
replay_full_input: 'Full replay input (JSON)',
|
|
828
|
+
replay_full_input_hint: 'Edit full source input including historyMessages/messages/streamParams.',
|
|
829
|
+
replay_full_input_invalid: 'Replay input JSON is invalid',
|
|
830
|
+
skills_title: 'Skills',
|
|
831
|
+
skill_name: 'Skill',
|
|
832
|
+
call_count: 'Calls',
|
|
833
|
+
run_count: 'Runs',
|
|
834
|
+
session_count: 'Sessions',
|
|
835
|
+
last_seen: 'Last Seen',
|
|
836
|
+
no_skills: 'No skills detected in selected scope/time range',
|
|
837
|
+
skill_detail: 'Skill Detail',
|
|
838
|
+
skill_description: 'Description',
|
|
839
|
+
skill_location: 'Location',
|
|
840
|
+
skill_content: 'SKILL.md Content',
|
|
841
|
+
recent_calls: 'Recent Calls',
|
|
842
|
+
status: 'Status',
|
|
843
|
+
success_short: 'OK',
|
|
844
|
+
fail_short: 'Failed',
|
|
845
|
+
no_recent_calls: 'No recent skill calls',
|
|
846
|
+
close: 'Close',
|
|
847
|
+
},
|
|
848
|
+
zh: {
|
|
849
|
+
nav_dashboard: '总览',
|
|
850
|
+
nav_trace: '链路',
|
|
851
|
+
nav_skills: '技能',
|
|
852
|
+
nav_replay: '回放',
|
|
853
|
+
nav_metrics: '指标',
|
|
854
|
+
nav_security: '安全',
|
|
855
|
+
top_control_panel: '控制台',
|
|
856
|
+
lang: '语言',
|
|
857
|
+
tenant_scope: 'Scope ID',
|
|
858
|
+
select_scope: '选择 Scope',
|
|
859
|
+
loading: '加载中...',
|
|
860
|
+
loading_dashboard: '加载看板中...',
|
|
861
|
+
loading_skills: '加载技能中...',
|
|
862
|
+
loading_metrics: '加载指标中...',
|
|
863
|
+
loading_replay: '加载回放工作区中...',
|
|
864
|
+
failed_load: '加载失败:',
|
|
865
|
+
sessions: '会话数',
|
|
866
|
+
actions: '动作数',
|
|
867
|
+
tokens: 'Token',
|
|
868
|
+
avg_latency: '平均耗时',
|
|
869
|
+
success: '成功率',
|
|
870
|
+
search_session: '搜索会话 ID、Key、用户、模型...',
|
|
871
|
+
search: '搜索',
|
|
872
|
+
clear: '清空',
|
|
873
|
+
refresh_trace_list: '刷新链路列表',
|
|
874
|
+
traces: '链路',
|
|
875
|
+
matched: '命中',
|
|
876
|
+
total: '总计',
|
|
877
|
+
no_sessions: '暂无会话数据',
|
|
878
|
+
main: '主代理',
|
|
879
|
+
subagent: '子代理',
|
|
880
|
+
system: '系统',
|
|
881
|
+
replay: '回放',
|
|
882
|
+
all: '全部',
|
|
883
|
+
filter_agent_type: '筛选代理类型',
|
|
884
|
+
start_time: '开始时间',
|
|
885
|
+
end_time: '结束时间',
|
|
886
|
+
page: '第',
|
|
887
|
+
prev: '上一页',
|
|
888
|
+
next: '下一页',
|
|
889
|
+
parent: '父会话',
|
|
890
|
+
heuristic_subagent_session: '启发式识别子代理会话',
|
|
891
|
+
actions_unit: '次动作',
|
|
892
|
+
tokens_unit: 'Token',
|
|
893
|
+
trace_detail_loading: '加载链路详情中...',
|
|
894
|
+
back_to_traces: '返回链路列表',
|
|
895
|
+
completed: '已完成',
|
|
896
|
+
failed: '失败',
|
|
897
|
+
running: '运行中',
|
|
898
|
+
duration: '耗时',
|
|
899
|
+
snapshots: '快照',
|
|
900
|
+
time: '时间',
|
|
901
|
+
search_this_session: '搜索本会话:动作、输入、输出、模型...',
|
|
902
|
+
refresh_current_trace: '刷新当前链路',
|
|
903
|
+
search_within_session: '在当前会话中搜索',
|
|
904
|
+
no_action_matched: '未匹配到动作',
|
|
905
|
+
try_hint: '可尝试工具名、模型名、Prompt 片段或错误文本。',
|
|
906
|
+
input: '输入',
|
|
907
|
+
output: '输出',
|
|
908
|
+
type: '类型',
|
|
909
|
+
model: '模型',
|
|
910
|
+
agent: '代理',
|
|
911
|
+
channel: '通道',
|
|
912
|
+
prompt_tokens: '提示词 Token',
|
|
913
|
+
completion_tokens: '输出 Token',
|
|
914
|
+
metrics_disabled: '已禁用',
|
|
915
|
+
refresh_metrics: '刷新指标',
|
|
916
|
+
apply_time_range: '应用时间范围',
|
|
917
|
+
start: '开始',
|
|
918
|
+
end: '结束',
|
|
919
|
+
latest_metrics_snapshot: '最新指标快照',
|
|
920
|
+
filter_by_metric_name: '按指标名筛选…',
|
|
921
|
+
no_replay_result: '暂无回放结果。',
|
|
922
|
+
run_replay: '执行回放',
|
|
923
|
+
replay_running: '执行中...',
|
|
924
|
+
replay_title: '回放',
|
|
925
|
+
replay_subtitle: 'LLM 沙箱',
|
|
926
|
+
replay_full_input: '完整回放参数(JSON)',
|
|
927
|
+
replay_full_input_hint: '可编辑完整源输入,包括 historyMessages/messages/streamParams。',
|
|
928
|
+
replay_full_input_invalid: '回放参数 JSON 不合法',
|
|
929
|
+
skills_title: '技能列表',
|
|
930
|
+
skill_name: '技能',
|
|
931
|
+
call_count: '调用次数',
|
|
932
|
+
run_count: '运行数',
|
|
933
|
+
session_count: '会话数',
|
|
934
|
+
last_seen: '最近调用',
|
|
935
|
+
no_skills: '当前 Scope/时间范围未识别到技能',
|
|
936
|
+
skill_detail: '技能详情',
|
|
937
|
+
skill_description: '描述',
|
|
938
|
+
skill_location: '路径',
|
|
939
|
+
skill_content: 'SKILL.md 内容',
|
|
940
|
+
recent_calls: '最近调用',
|
|
941
|
+
status: '状态',
|
|
942
|
+
success_short: '成功',
|
|
943
|
+
fail_short: '失败',
|
|
944
|
+
no_recent_calls: '暂无技能调用记录',
|
|
945
|
+
close: '关闭',
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
function t(key) {
|
|
949
|
+
var pack = I18N[currentLang] || I18N.en;
|
|
950
|
+
return String((pack && pack[key]) || I18N.en[key] || key);
|
|
951
|
+
}
|
|
952
|
+
function tr(enText, zhText) {
|
|
953
|
+
return currentLang === 'zh' ? zhText : enText;
|
|
954
|
+
}
|
|
566
955
|
|
|
567
956
|
var TIME_PRESETS = [
|
|
568
|
-
{ key:'30m',
|
|
569
|
-
{ key:'1h',
|
|
570
|
-
{ key:'6h',
|
|
571
|
-
{ key:'24h',
|
|
572
|
-
{ key:'3d',
|
|
573
|
-
{ key:'7d',
|
|
574
|
-
{ key:'14d',
|
|
575
|
-
{ key:'1mo',
|
|
576
|
-
{ key:'3mo',
|
|
577
|
-
{ key:'',
|
|
957
|
+
{ key:'30m', labelEn:'30 min', labelZh:'近 30 分钟', ms: 30*60*1000 },
|
|
958
|
+
{ key:'1h', labelEn:'1 hour', labelZh:'近 1 小时', ms: 60*60*1000 },
|
|
959
|
+
{ key:'6h', labelEn:'6 hours', labelZh:'近 6 小时', ms: 6*60*60*1000 },
|
|
960
|
+
{ key:'24h', labelEn:'24 hours', labelZh:'近 24 小时', ms: 24*60*60*1000 },
|
|
961
|
+
{ key:'3d', labelEn:'3 days', labelZh:'近 3 天', ms: 3*24*60*60*1000 },
|
|
962
|
+
{ key:'7d', labelEn:'7 days', labelZh:'近 7 天', ms: 7*24*60*60*1000 },
|
|
963
|
+
{ key:'14d', labelEn:'14 days', labelZh:'近 14 天', ms: 14*24*60*60*1000 },
|
|
964
|
+
{ key:'1mo', labelEn:'1 month', labelZh:'近 1 个月', ms: 30*24*60*60*1000 },
|
|
965
|
+
{ key:'3mo', labelEn:'3 months', labelZh:'近 3 个月', ms: 90*24*60*60*1000 },
|
|
966
|
+
{ key:'', labelEn:'All time', labelZh:'全部时间', ms: 0 }
|
|
578
967
|
];
|
|
968
|
+
function timePresetLabel(preset) {
|
|
969
|
+
if (!preset) return currentLang === 'zh' ? '全部时间' : 'All time';
|
|
970
|
+
return currentLang === 'zh' ? String(preset.labelZh || preset.labelEn || '') : String(preset.labelEn || preset.labelZh || '');
|
|
971
|
+
}
|
|
579
972
|
|
|
580
973
|
function getTimeFromISO() {
|
|
581
974
|
if (!filterTimeRange) return '';
|
|
@@ -584,10 +977,169 @@ function getTimeFromISO() {
|
|
|
584
977
|
return new Date(Date.now() - preset.ms).toISOString();
|
|
585
978
|
}
|
|
586
979
|
|
|
980
|
+
function getTimeToISO() {
|
|
981
|
+
if (!filterTimeRange) return '';
|
|
982
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === filterTimeRange; });
|
|
983
|
+
if (!preset || !preset.ms) return '';
|
|
984
|
+
return new Date().toISOString();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function hasCustomTimeRange() {
|
|
988
|
+
return !!(filterTimeFrom || filterTimeTo);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function getTraceTimeFilter() {
|
|
992
|
+
if (hasCustomTimeRange()) {
|
|
993
|
+
return {
|
|
994
|
+
timeFrom: filterTimeFrom || '',
|
|
995
|
+
timeTo: filterTimeTo || '',
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
timeFrom: getTimeFromISO(),
|
|
1000
|
+
timeTo: getTimeToISO(),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function toLocalDateTimeInputValue(isoText) {
|
|
1005
|
+
if (!isoText) return '';
|
|
1006
|
+
var d = new Date(isoText);
|
|
1007
|
+
if (!Number.isFinite(d.getTime())) return '';
|
|
1008
|
+
var y = d.getFullYear();
|
|
1009
|
+
var m = String(d.getMonth() + 1).padStart(2, '0');
|
|
1010
|
+
var day = String(d.getDate()).padStart(2, '0');
|
|
1011
|
+
var hh = String(d.getHours()).padStart(2, '0');
|
|
1012
|
+
var mm = String(d.getMinutes()).padStart(2, '0');
|
|
1013
|
+
return y + '-' + m + '-' + day + 'T' + hh + ':' + mm;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function toISOFromDateTimeInputValue(localVal) {
|
|
1017
|
+
if (!localVal) return '';
|
|
1018
|
+
var d = new Date(localVal);
|
|
1019
|
+
if (!Number.isFinite(d.getTime())) return '';
|
|
1020
|
+
return d.toISOString();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
587
1023
|
function getTimeLabel() {
|
|
588
|
-
if (
|
|
1024
|
+
if (hasCustomTimeRange()) return currentLang === 'zh' ? '自定义' : 'Custom';
|
|
1025
|
+
if (!filterTimeRange) return currentLang === 'zh' ? '全部时间' : 'All time';
|
|
589
1026
|
var preset = TIME_PRESETS.find(function(p){ return p.key === filterTimeRange; });
|
|
590
|
-
return preset ? preset
|
|
1027
|
+
return preset ? timePresetLabel(preset) : (currentLang === 'zh' ? '全部时间' : 'All time');
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function getTimeRangePreview() {
|
|
1031
|
+
if (hasCustomTimeRange()) {
|
|
1032
|
+
var s = filterTimeFrom ? new Date(filterTimeFrom).toLocaleString() : '-';
|
|
1033
|
+
var e = filterTimeTo ? new Date(filterTimeTo).toLocaleString() : '-';
|
|
1034
|
+
return s + ' - ' + e;
|
|
1035
|
+
}
|
|
1036
|
+
if (!filterTimeRange) return 'All time';
|
|
1037
|
+
var from = getTimeFromISO();
|
|
1038
|
+
if (!from) return 'All time';
|
|
1039
|
+
var to = getTimeToISO();
|
|
1040
|
+
var start = new Date(from).toLocaleString();
|
|
1041
|
+
var end = to ? new Date(to).toLocaleString() : new Date().toLocaleString();
|
|
1042
|
+
return start + ' - ' + end;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function getMetricsWindowLabel() {
|
|
1046
|
+
return String(metricsRangeMinutes) + ' min';
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function tenantLabel(t) {
|
|
1050
|
+
return (t && t.scope_id) ? String(t.scope_id) : 'local';
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function uniqueStrings(rows, getter) {
|
|
1054
|
+
var set = {};
|
|
1055
|
+
var out = [];
|
|
1056
|
+
(rows || []).forEach(function(r) {
|
|
1057
|
+
var v = String(getter(r) || '').trim();
|
|
1058
|
+
if (!v) return;
|
|
1059
|
+
if (set[v]) return;
|
|
1060
|
+
set[v] = true;
|
|
1061
|
+
out.push(v);
|
|
1062
|
+
});
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function loadSavedScopeId() {
|
|
1067
|
+
try {
|
|
1068
|
+
var v = String(window.localStorage.getItem(SCOPE_STORAGE_KEY) || '').trim();
|
|
1069
|
+
return v || '';
|
|
1070
|
+
} catch (_) {
|
|
1071
|
+
return '';
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function saveScopeId(v) {
|
|
1076
|
+
try {
|
|
1077
|
+
window.localStorage.setItem(SCOPE_STORAGE_KEY, String(v || 'local'));
|
|
1078
|
+
} catch (_) {}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function getTenantScopes() {
|
|
1082
|
+
var all = uniqueStrings(tenantOptions, function(t) { return t && t.scope_id; });
|
|
1083
|
+
return all.length ? all : ['local'];
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function syncTenantSelection() {
|
|
1087
|
+
var scopes = getTenantScopes();
|
|
1088
|
+
if (scopes.indexOf(selectedScopeId) < 0) selectedScopeId = scopes[0];
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function ensureTenantsLoaded() {
|
|
1092
|
+
if (!SHOW_TENANT_SCOPE) {
|
|
1093
|
+
tenantOptions = [];
|
|
1094
|
+
selectedScopeId = 'local';
|
|
1095
|
+
defaultScopeId = 'local';
|
|
1096
|
+
tenantsLoaded = true;
|
|
1097
|
+
tenantsLoading = null;
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (tenantsLoaded) return;
|
|
1101
|
+
if (tenantsLoading) {
|
|
1102
|
+
await tenantsLoading;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
tenantsLoading = (async function() {
|
|
1106
|
+
var rerenderNeeded = false;
|
|
1107
|
+
try {
|
|
1108
|
+
var res = await fetchApi('/tenants', { skipScope: true });
|
|
1109
|
+
var rows = (res && Array.isArray(res.tenants)) ? res.tenants.slice() : [];
|
|
1110
|
+
tenantOptions = rows;
|
|
1111
|
+
var scopes = rows.map(function(t) { return String((t && t.scope_id) || '').trim(); }).filter(Boolean);
|
|
1112
|
+
var current = String(selectedScopeId || '').trim();
|
|
1113
|
+
var saved = loadSavedScopeId();
|
|
1114
|
+
if (current && scopes.indexOf(current) >= 0) selectedScopeId = current;
|
|
1115
|
+
else if (saved && scopes.indexOf(saved) >= 0) selectedScopeId = saved;
|
|
1116
|
+
else {
|
|
1117
|
+
var preferred = rows.find(function(t) {
|
|
1118
|
+
return String((t && t.scope_id) || '').toLowerCase() === 'local';
|
|
1119
|
+
});
|
|
1120
|
+
if (preferred && preferred.scope_id) selectedScopeId = String(preferred.scope_id);
|
|
1121
|
+
else if (rows.length > 0 && rows[0].scope_id) selectedScopeId = String(rows[0].scope_id);
|
|
1122
|
+
else selectedScopeId = 'local';
|
|
1123
|
+
}
|
|
1124
|
+
syncTenantSelection();
|
|
1125
|
+
saveScopeId(selectedScopeId);
|
|
1126
|
+
defaultScopeId = selectedScopeId;
|
|
1127
|
+
rerenderNeeded = true;
|
|
1128
|
+
} catch (_) {
|
|
1129
|
+
tenantOptions = [];
|
|
1130
|
+
selectedScopeId = 'local';
|
|
1131
|
+
defaultScopeId = 'local';
|
|
1132
|
+
} finally {
|
|
1133
|
+
tenantsLoaded = true;
|
|
1134
|
+
tenantsLoading = null;
|
|
1135
|
+
if (rerenderNeeded) {
|
|
1136
|
+
setTimeout(function() {
|
|
1137
|
+
try { router(); } catch (_) {}
|
|
1138
|
+
}, 0);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
})();
|
|
1142
|
+
await tenantsLoading;
|
|
591
1143
|
}
|
|
592
1144
|
|
|
593
1145
|
/* ---------- theme ---------- */
|
|
@@ -609,6 +1161,9 @@ function toggleTheme() {
|
|
|
609
1161
|
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
610
1162
|
}
|
|
611
1163
|
applyTheme(getTheme());
|
|
1164
|
+
if (SHOW_TENANT_SCOPE) {
|
|
1165
|
+
ensureTenantsLoaded().catch(function() {});
|
|
1166
|
+
}
|
|
612
1167
|
|
|
613
1168
|
/* ---------- helpers ---------- */
|
|
614
1169
|
function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
@@ -788,6 +1343,7 @@ function getToolCallId(action) {
|
|
|
788
1343
|
}
|
|
789
1344
|
function getActionRunId(action) {
|
|
790
1345
|
if (!action) return '';
|
|
1346
|
+
if (typeof action.run_id === 'string' && action.run_id) return action.run_id;
|
|
791
1347
|
var input = parseJson(action.input_params);
|
|
792
1348
|
if (input && typeof input === 'object' && typeof input.runId === 'string' && input.runId) {
|
|
793
1349
|
return input.runId;
|
|
@@ -798,6 +1354,174 @@ function getActionRunId(action) {
|
|
|
798
1354
|
}
|
|
799
1355
|
return '';
|
|
800
1356
|
}
|
|
1357
|
+
function getActionParentRunId(action) {
|
|
1358
|
+
if (!action) return '';
|
|
1359
|
+
if (typeof action.parent_run_id === 'string' && action.parent_run_id) return action.parent_run_id;
|
|
1360
|
+
var input = parseJson(action.input_params);
|
|
1361
|
+
if (input && typeof input === 'object' && typeof input.parentRunId === 'string' && input.parentRunId) {
|
|
1362
|
+
return input.parentRunId;
|
|
1363
|
+
}
|
|
1364
|
+
var output = parseJson(action.output_result);
|
|
1365
|
+
if (output && typeof output === 'object' && typeof output.parentRunId === 'string' && output.parentRunId) {
|
|
1366
|
+
return output.parentRunId;
|
|
1367
|
+
}
|
|
1368
|
+
return '';
|
|
1369
|
+
}
|
|
1370
|
+
function parseJsonObjectFromText(text) {
|
|
1371
|
+
if (typeof text !== 'string') return null;
|
|
1372
|
+
var t = text.trim();
|
|
1373
|
+
if (!t) return null;
|
|
1374
|
+
if (t.charAt(0) !== '{' && t.charAt(0) !== '[') return null;
|
|
1375
|
+
try {
|
|
1376
|
+
var parsed = JSON.parse(t);
|
|
1377
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
1378
|
+
} catch (_) {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function readSessionsSpawnDetails(outputObj) {
|
|
1383
|
+
if (!outputObj || typeof outputObj !== 'object') return null;
|
|
1384
|
+
if (outputObj.details && typeof outputObj.details === 'object') {
|
|
1385
|
+
return outputObj.details;
|
|
1386
|
+
}
|
|
1387
|
+
if (Array.isArray(outputObj.content)) {
|
|
1388
|
+
for (var i = 0; i < outputObj.content.length; i++) {
|
|
1389
|
+
var item = outputObj.content[i];
|
|
1390
|
+
if (!item || typeof item !== 'object') continue;
|
|
1391
|
+
if (item.type !== 'text' || typeof item.text !== 'string') continue;
|
|
1392
|
+
var parsed = parseJsonObjectFromText(item.text);
|
|
1393
|
+
if (parsed && typeof parsed === 'object' && parsed.childSessionKey) {
|
|
1394
|
+
return parsed;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
function shortSessionKey(v) {
|
|
1401
|
+
var s = String(v || '');
|
|
1402
|
+
if (!s) return '';
|
|
1403
|
+
if (s.length <= 40) return s;
|
|
1404
|
+
return s.slice(0, 22) + '...' + s.slice(-14);
|
|
1405
|
+
}
|
|
1406
|
+
function getSessionsSpawnInfo(action) {
|
|
1407
|
+
if (!action || action.action_name !== 'tool_call:sessions_spawn') return null;
|
|
1408
|
+
var input = parseJson(action.input_params);
|
|
1409
|
+
if (!input || typeof input !== 'object') input = {};
|
|
1410
|
+
var output = parseJson(action.output_result);
|
|
1411
|
+
if (!output || typeof output !== 'object') output = {};
|
|
1412
|
+
var details = readSessionsSpawnDetails(output) || {};
|
|
1413
|
+
var childSessionKey = String(details.childSessionKey || '').trim();
|
|
1414
|
+
var childRunId = String(details.runId || '').trim();
|
|
1415
|
+
var label = String(input.label || '').trim();
|
|
1416
|
+
var task = String(input.task || '').trim();
|
|
1417
|
+
var runtime = String(input.runtime || '').trim();
|
|
1418
|
+
if (!childSessionKey && !label && !task) return null;
|
|
1419
|
+
return {
|
|
1420
|
+
childSessionKey: childSessionKey,
|
|
1421
|
+
childRunId: childRunId,
|
|
1422
|
+
label: label,
|
|
1423
|
+
task: task,
|
|
1424
|
+
runtime: runtime,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function formatTraceActionName(action) {
|
|
1428
|
+
if (!action) return '';
|
|
1429
|
+
var runId = getActionRunId(action) || '';
|
|
1430
|
+
var announceInfo = parseAnnounceRunInfo(runId);
|
|
1431
|
+
if (announceInfo && action.action_type === 'message' && String(action.action_name || '').indexOf('llm_call:') === 0) {
|
|
1432
|
+
return 'subagent_callback -> ' + shortSessionKey(announceInfo.childSessionKey);
|
|
1433
|
+
}
|
|
1434
|
+
if (action.action_name === 'tool_start:sessions_spawn') {
|
|
1435
|
+
return 'subagent_spawn_start';
|
|
1436
|
+
}
|
|
1437
|
+
if (action.action_name === 'tool_call:sessions_spawn') {
|
|
1438
|
+
var info = getSessionsSpawnInfo(action);
|
|
1439
|
+
if (info && info.childSessionKey) {
|
|
1440
|
+
return 'subagent_spawn_request -> ' + shortSessionKey(info.childSessionKey);
|
|
1441
|
+
}
|
|
1442
|
+
return 'subagent_spawn_request';
|
|
1443
|
+
}
|
|
1444
|
+
if (action.action_name === 'tool_start:sessions_yield') {
|
|
1445
|
+
return 'subagent_wait_start';
|
|
1446
|
+
}
|
|
1447
|
+
if (action.action_name === 'tool_call:sessions_yield') {
|
|
1448
|
+
return 'subagent_wait';
|
|
1449
|
+
}
|
|
1450
|
+
return String(action.action_name || '');
|
|
1451
|
+
}
|
|
1452
|
+
function buildTraceSubagentSummary(actions) {
|
|
1453
|
+
var list = [];
|
|
1454
|
+
var byKey = {};
|
|
1455
|
+
ensureArray(actions).forEach(function(a) {
|
|
1456
|
+
var info = getSessionsSpawnInfo(a);
|
|
1457
|
+
if (!info || !info.childSessionKey) return;
|
|
1458
|
+
var key = info.childSessionKey;
|
|
1459
|
+
var entry = byKey[key];
|
|
1460
|
+
if (!entry) {
|
|
1461
|
+
entry = {
|
|
1462
|
+
childSessionKey: key,
|
|
1463
|
+
childSessionId: '',
|
|
1464
|
+
childRunId: info.childRunId || '',
|
|
1465
|
+
label: info.label || '',
|
|
1466
|
+
task: info.task || '',
|
|
1467
|
+
runtime: info.runtime || '',
|
|
1468
|
+
spawnedAt: a.created_at || '',
|
|
1469
|
+
status: 'running',
|
|
1470
|
+
completedAt: '',
|
|
1471
|
+
};
|
|
1472
|
+
byKey[key] = entry;
|
|
1473
|
+
list.push(entry);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (!entry.childRunId && info.childRunId) entry.childRunId = info.childRunId;
|
|
1477
|
+
if (!entry.label && info.label) entry.label = info.label;
|
|
1478
|
+
if (!entry.task && info.task) entry.task = info.task;
|
|
1479
|
+
if (!entry.runtime && info.runtime) entry.runtime = info.runtime;
|
|
1480
|
+
});
|
|
1481
|
+
if (!list.length) return [];
|
|
1482
|
+
|
|
1483
|
+
var completionSessionIdRegex = /session_id:\\s*([0-9a-fA-F-]{36})/;
|
|
1484
|
+
ensureArray(actions).forEach(function(a) {
|
|
1485
|
+
var hay = stringifySearchValue(a.input_params) + '\\n' + stringifySearchValue(a.output_result);
|
|
1486
|
+
var lower = hay.toLowerCase();
|
|
1487
|
+
list.forEach(function(entry) {
|
|
1488
|
+
if (!entry || !entry.childSessionKey) return;
|
|
1489
|
+
if (lower.indexOf(entry.childSessionKey.toLowerCase()) < 0) return;
|
|
1490
|
+
var m = hay.match(completionSessionIdRegex);
|
|
1491
|
+
if (m && m[1] && !entry.childSessionId) entry.childSessionId = m[1];
|
|
1492
|
+
if (lower.indexOf('completed successfully') >= 0 || lower.indexOf('status: completed') >= 0) {
|
|
1493
|
+
entry.status = 'done';
|
|
1494
|
+
if (!entry.completedAt) entry.completedAt = a.created_at || '';
|
|
1495
|
+
} else if (lower.indexOf('failed') >= 0 || lower.indexOf('status: error') >= 0) {
|
|
1496
|
+
entry.status = 'fail';
|
|
1497
|
+
if (!entry.completedAt) entry.completedAt = a.created_at || '';
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
return list;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function parseAnnounceChildRunId(runId) {
|
|
1506
|
+
var s = String(runId || '');
|
|
1507
|
+
if (s.indexOf('announce:v1:') !== 0) return '';
|
|
1508
|
+
var last = s.lastIndexOf(':');
|
|
1509
|
+
if (last < 0 || last >= s.length - 1) return '';
|
|
1510
|
+
var tail = s.slice(last + 1);
|
|
1511
|
+
return /^[0-9a-fA-F-]{16,}$/.test(tail) ? tail : '';
|
|
1512
|
+
}
|
|
1513
|
+
function parseAnnounceRunInfo(runId) {
|
|
1514
|
+
var s = String(runId || '');
|
|
1515
|
+
if (s.indexOf('announce:v1:') !== 0) return null;
|
|
1516
|
+
var childRunId = parseAnnounceChildRunId(s);
|
|
1517
|
+
if (!childRunId) return null;
|
|
1518
|
+
var prefix = 'announce:v1:';
|
|
1519
|
+
var cut = s.length - childRunId.length - 1;
|
|
1520
|
+
if (cut <= prefix.length) return null;
|
|
1521
|
+
var childSessionKey = s.slice(prefix.length, cut);
|
|
1522
|
+
if (!childSessionKey) return null;
|
|
1523
|
+
return { childSessionKey: childSessionKey, childRunId: childRunId };
|
|
1524
|
+
}
|
|
801
1525
|
function normalizeTraceSearch(s) {
|
|
802
1526
|
return (s || '').trim().toLowerCase();
|
|
803
1527
|
}
|
|
@@ -1204,13 +1928,20 @@ function mapGetApiToRpc(path) {
|
|
|
1204
1928
|
var u = new URL(path, 'http://local');
|
|
1205
1929
|
var p = u.pathname;
|
|
1206
1930
|
var q = u.searchParams;
|
|
1931
|
+
if (p === '/tenants') {
|
|
1932
|
+
return {
|
|
1933
|
+
method: 'observability.tenants',
|
|
1934
|
+
params: {}
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1207
1937
|
|
|
1208
1938
|
if (p === '/stats') {
|
|
1209
1939
|
return {
|
|
1210
1940
|
method: 'observability.stats',
|
|
1211
1941
|
params: {
|
|
1212
1942
|
timeFrom: q.get('timeFrom') || undefined,
|
|
1213
|
-
timeTo: q.get('timeTo') || undefined
|
|
1943
|
+
timeTo: q.get('timeTo') || undefined,
|
|
1944
|
+
agentType: q.get('agentType') || undefined
|
|
1214
1945
|
}
|
|
1215
1946
|
};
|
|
1216
1947
|
}
|
|
@@ -1224,6 +1955,7 @@ function mapGetApiToRpc(path) {
|
|
|
1224
1955
|
model: q.get('model') || undefined,
|
|
1225
1956
|
sessionId: q.get('sessionId') || undefined,
|
|
1226
1957
|
search: q.get('search') || undefined,
|
|
1958
|
+
agentType: q.get('agentType') || undefined,
|
|
1227
1959
|
timeFrom: q.get('timeFrom') || undefined,
|
|
1228
1960
|
timeTo: q.get('timeTo') || undefined
|
|
1229
1961
|
}
|
|
@@ -1312,6 +2044,27 @@ function mapGetApiToRpc(path) {
|
|
|
1312
2044
|
}
|
|
1313
2045
|
};
|
|
1314
2046
|
}
|
|
2047
|
+
if (p === '/skills/overview') {
|
|
2048
|
+
return {
|
|
2049
|
+
method: 'observability.skills.overview',
|
|
2050
|
+
params: {
|
|
2051
|
+
limit: toIntOrUndef(q.get('limit')),
|
|
2052
|
+
timeFrom: q.get('timeFrom') || undefined,
|
|
2053
|
+
timeTo: q.get('timeTo') || undefined
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
if (p === '/skills/detail') {
|
|
2058
|
+
return {
|
|
2059
|
+
method: 'observability.skills.detail',
|
|
2060
|
+
params: {
|
|
2061
|
+
skill: q.get('skill') || '',
|
|
2062
|
+
limit: toIntOrUndef(q.get('limit')),
|
|
2063
|
+
timeFrom: q.get('timeFrom') || undefined,
|
|
2064
|
+
timeTo: q.get('timeTo') || undefined
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
1315
2068
|
if (p === '/security/config') return { method: 'observability.security.config.get', params: {} };
|
|
1316
2069
|
if (p === '/metrics/overview') {
|
|
1317
2070
|
return {
|
|
@@ -1337,6 +2090,12 @@ function mapGetApiToRpc(path) {
|
|
|
1337
2090
|
}
|
|
1338
2091
|
};
|
|
1339
2092
|
}
|
|
2093
|
+
if (p === '/llm/providers') {
|
|
2094
|
+
return {
|
|
2095
|
+
method: 'observability.llm.providers',
|
|
2096
|
+
params: {}
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
1340
2099
|
return null;
|
|
1341
2100
|
}
|
|
1342
2101
|
|
|
@@ -1349,6 +2108,12 @@ function mapPostApiToRpc(path, payload) {
|
|
|
1349
2108
|
params: payload || {}
|
|
1350
2109
|
};
|
|
1351
2110
|
}
|
|
2111
|
+
if (p === '/llm/replay') {
|
|
2112
|
+
return {
|
|
2113
|
+
method: 'observability.llm.replay',
|
|
2114
|
+
params: payload || {}
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
1352
2117
|
var m = p.match(/^\\/alerts\\/([^/]+)\\/status$/);
|
|
1353
2118
|
if (m) {
|
|
1354
2119
|
return {
|
|
@@ -1414,14 +2179,27 @@ async function postApiHttp(path, payload) {
|
|
|
1414
2179
|
}
|
|
1415
2180
|
|
|
1416
2181
|
async function fetchApi(path) {
|
|
2182
|
+
var options = arguments.length > 1 ? arguments[1] : null;
|
|
1417
2183
|
var mapped = mapGetApiToRpc(path);
|
|
1418
2184
|
if (!mapped) throw new Error('Unsupported API path (WS-only): ' + path);
|
|
2185
|
+
if (!(options && options.skipScope) && mapped.method !== 'observability.tenants') {
|
|
2186
|
+
if (!mapped.params) mapped.params = {};
|
|
2187
|
+
if (!mapped.params.scopeId) mapped.params.scopeId = selectedScopeId || 'local';
|
|
2188
|
+
}
|
|
1419
2189
|
try {
|
|
1420
2190
|
return await getGatewayWsClient().request(mapped.method, mapped.params || {});
|
|
1421
2191
|
} catch (err) {
|
|
1422
2192
|
var wsDetail = (err && err.message) ? String(err.message) : 'unknown';
|
|
1423
2193
|
try {
|
|
1424
|
-
|
|
2194
|
+
var fallbackPath = path;
|
|
2195
|
+
if (!(options && options.skipScope) && mapped.method !== 'observability.tenants') {
|
|
2196
|
+
var scope = String((mapped.params && mapped.params.scopeId) || selectedScopeId || 'local');
|
|
2197
|
+
var sep = fallbackPath.indexOf('?') >= 0 ? '&' : '?';
|
|
2198
|
+
if (fallbackPath.indexOf('scopeId=') < 0 && fallbackPath.indexOf('scope_id=') < 0) {
|
|
2199
|
+
fallbackPath += sep + 'scopeId=' + encodeURIComponent(scope);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return await fetchApiHttp(fallbackPath);
|
|
1425
2203
|
} catch (httpErr) {
|
|
1426
2204
|
var httpDetail = (httpErr && httpErr.message) ? String(httpErr.message) : 'unknown';
|
|
1427
2205
|
throw new Error('WS request failed for ' + mapped.method + ': ' + wsDetail + '; HTTP fallback failed: ' + httpDetail);
|
|
@@ -1432,6 +2210,10 @@ async function fetchApi(path) {
|
|
|
1432
2210
|
async function postApi(path, payload) {
|
|
1433
2211
|
var mapped = mapPostApiToRpc(path, payload);
|
|
1434
2212
|
if (!mapped) throw new Error('Unsupported API path (WS-only): ' + path);
|
|
2213
|
+
if (mapped.method !== 'observability.security.config.update') {
|
|
2214
|
+
if (!mapped.params) mapped.params = {};
|
|
2215
|
+
if (!mapped.params.scopeId) mapped.params.scopeId = selectedScopeId || 'local';
|
|
2216
|
+
}
|
|
1435
2217
|
try {
|
|
1436
2218
|
return await getGatewayWsClient().request(mapped.method, mapped.params || {});
|
|
1437
2219
|
} catch (err) {
|
|
@@ -1451,6 +2233,7 @@ var ICON_SUN = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d=
|
|
|
1451
2233
|
var ICON_MOON = '<svg viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>';
|
|
1452
2234
|
var ICON_ACTIVITY = '<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
|
|
1453
2235
|
var ICON_REFRESH = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 4v6h-6"/><path d="M4 20v-6h6"/><path d="M19.2 14A8 8 0 0 1 6.1 18.3L4 14"/><path d="M4.8 10A8 8 0 0 1 17.9 5.7L20 10"/></svg>';
|
|
2236
|
+
var ICON_CHECK = '<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
1454
2237
|
|
|
1455
2238
|
/* ---------- router ---------- */
|
|
1456
2239
|
function parseHashParams(hash) {
|
|
@@ -1475,6 +2258,8 @@ function router() {
|
|
|
1475
2258
|
renderTraceList();
|
|
1476
2259
|
} else if (hash.indexOf('#/security') === 0) {
|
|
1477
2260
|
renderSecurity();
|
|
2261
|
+
} else if (hash.indexOf('#/skills') === 0) {
|
|
2262
|
+
renderSkills();
|
|
1478
2263
|
} else if (hash.indexOf('#/metrics') === 0) {
|
|
1479
2264
|
if (!SHOW_METRICS_PAGE) {
|
|
1480
2265
|
try {
|
|
@@ -1486,6 +2271,8 @@ function router() {
|
|
|
1486
2271
|
return;
|
|
1487
2272
|
}
|
|
1488
2273
|
renderMetrics();
|
|
2274
|
+
} else if (hash.indexOf('#/replay') === 0) {
|
|
2275
|
+
renderReplay();
|
|
1489
2276
|
} else {
|
|
1490
2277
|
renderDashboard();
|
|
1491
2278
|
}
|
|
@@ -1498,29 +2285,57 @@ function renderLayout(active, content) {
|
|
|
1498
2285
|
var isTrace = active === 'trace';
|
|
1499
2286
|
var shellCls = isTrace ? 'shell shell--trace' : 'shell';
|
|
1500
2287
|
var contentCls = isTrace ? 'content content--trace' : 'content';
|
|
2288
|
+
if (SHOW_TENANT_SCOPE) syncTenantSelection();
|
|
2289
|
+
var scopes = SHOW_TENANT_SCOPE ? getTenantScopes() : [];
|
|
2290
|
+
var projectSelect = '<div class="tenant-field"><div class="tenant-label">' + esc(t('tenant_scope')) + '</div><div class="tenant-dropdown" id="tenant-project-dd"><button class="time-btn tenant-btn" onclick="toggleTenantMenu(event,\\'project\\')" aria-label="' + esc(t('select_scope')) + '">' + esc(selectedScopeId) + ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg></button><div class="time-menu tenant-menu" id="tenant-project-menu">';
|
|
2291
|
+
scopes.forEach(function(scopeId) {
|
|
2292
|
+
projectSelect += '<div class="time-menu-item' + (scopeId === selectedScopeId ? ' active' : '') + '" onclick="selectTenantScopeMenu(\\'' + esc(scopeId) + '\\')"><span class="check">' + (scopeId === selectedScopeId ? '✓' : '') + '</span>' + esc(scopeId) + '</div>';
|
|
2293
|
+
});
|
|
2294
|
+
projectSelect += '</div></div></div>';
|
|
2295
|
+
var languageLabel = currentLang === 'zh' ? '中文' : 'English';
|
|
2296
|
+
var langSelect =
|
|
2297
|
+
'<div class="tenant-dropdown" id="lang-dd">' +
|
|
2298
|
+
'<button class="time-btn tenant-btn" onclick="toggleLanguageMenu(event)" aria-label="' + esc(t('lang')) + '">' + esc(languageLabel) + ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg></button>' +
|
|
2299
|
+
'<div class="time-menu tenant-menu" id="lang-menu">' +
|
|
2300
|
+
'<div class="time-menu-item' + (currentLang === 'zh' ? ' active' : '') + '" onclick="selectLanguageMenu(\\'zh\\')"><span class="check">' + (currentLang === 'zh' ? '✓' : '') + '</span>中文</div>' +
|
|
2301
|
+
'<div class="time-menu-item' + (currentLang === 'en' ? ' active' : '') + '" onclick="selectLanguageMenu(\\'en\\')"><span class="check">' + (currentLang === 'en' ? '✓' : '') + '</span>English</div>' +
|
|
2302
|
+
'</div>' +
|
|
2303
|
+
'</div>';
|
|
1501
2304
|
return '<div class="' + shellCls + '">' +
|
|
1502
|
-
'<div class="
|
|
1503
|
-
'<
|
|
1504
|
-
'<div class="
|
|
1505
|
-
'<div class="brand
|
|
1506
|
-
|
|
1507
|
-
'<div class="brand-
|
|
1508
|
-
|
|
2305
|
+
'<div class="app-shell">' +
|
|
2306
|
+
'<aside class="side-nav">' +
|
|
2307
|
+
'<div class="side-nav-head">' +
|
|
2308
|
+
'<div class="brand">' +
|
|
2309
|
+
'<div class="brand-logo">' + ICON_ACTIVITY + '</div>' +
|
|
2310
|
+
'<div class="brand-text">' +
|
|
2311
|
+
'<div class="brand-title">OpenClaw</div>' +
|
|
2312
|
+
'<div class="brand-sub">Observability</div>' +
|
|
2313
|
+
'</div>' +
|
|
1509
2314
|
'</div>' +
|
|
1510
2315
|
'</div>' +
|
|
1511
|
-
'<
|
|
1512
|
-
'<a href="#/" class="' + (active==='dashboard'?'active':'') + '">
|
|
1513
|
-
'<a href="#/traces" class="' + (active==='traces'?'active':'') + '">
|
|
1514
|
-
|
|
1515
|
-
'<a href="#/
|
|
1516
|
-
|
|
2316
|
+
'<nav class="side-nav-links">' +
|
|
2317
|
+
'<a href="#/" class="' + (active==='dashboard'?'active':'') + '">' + esc(t('nav_dashboard')) + '</a>' +
|
|
2318
|
+
'<a href="#/traces" class="' + ((active==='traces' || active==='trace')?'active':'') + '">' + esc(t('nav_trace')) + '</a>' +
|
|
2319
|
+
'<a href="#/skills" class="' + (active==='skills'?'active':'') + '">' + esc(t('nav_skills')) + '</a>' +
|
|
2320
|
+
'<a href="#/replay" class="' + (active==='replay'?'active':'') + '">' + esc(t('nav_replay')) + '</a>' +
|
|
2321
|
+
(SHOW_METRICS_PAGE ? ('<a href="#/metrics" class="' + (active==='metrics'?'active':'') + '">' + esc(t('nav_metrics')) + '</a>') : '') +
|
|
2322
|
+
'<a href="#/security" class="' + (active==='security'?'active':'') + '">' + esc(t('nav_security')) + (window.__alertCount > 0 ? '<span class="nav-badge">' + window.__alertCount + '</span>' : '') + '</a>' +
|
|
2323
|
+
'</nav>' +
|
|
2324
|
+
'</aside>' +
|
|
2325
|
+
'<main class="main-shell">' +
|
|
2326
|
+
'<div class="topbar">' +
|
|
2327
|
+
'<div class="topbar-left">' +
|
|
2328
|
+
(SHOW_TENANT_SCOPE ? projectSelect : '') +
|
|
1517
2329
|
'</div>' +
|
|
1518
2330
|
'<div class="topbar-right">' +
|
|
2331
|
+
langSelect +
|
|
1519
2332
|
'<button class="theme-btn" onclick="toggleTheme();router()" title="Toggle theme">' + themeIcon + '</button>' +
|
|
1520
|
-
'<a href="/" class="back-link">' + ICON_BACK + '
|
|
2333
|
+
'<a href="/" class="back-link">' + ICON_BACK + ' ' + esc(t('top_control_panel')) + '</a>' +
|
|
1521
2334
|
'</div>' +
|
|
1522
2335
|
'</div>' +
|
|
1523
2336
|
'<div class="' + contentCls + '"><div class="content-inner">' + content + '</div></div>' +
|
|
2337
|
+
'</main>' +
|
|
2338
|
+
'</div>' +
|
|
1524
2339
|
'</div>';
|
|
1525
2340
|
}
|
|
1526
2341
|
|
|
@@ -1529,17 +2344,24 @@ function renderLayout(active, content) {
|
|
|
1529
2344
|
/* ================================================================ */
|
|
1530
2345
|
|
|
1531
2346
|
async function renderTraceList() {
|
|
1532
|
-
app.innerHTML = renderLayout('traces', '<div class="loading">
|
|
2347
|
+
app.innerHTML = renderLayout('traces', '<div class="loading">' + esc(t('loading')) + '</div>');
|
|
1533
2348
|
|
|
1534
2349
|
try {
|
|
2350
|
+
await ensureTenantsLoaded();
|
|
1535
2351
|
// Build sessions request with filter params
|
|
1536
2352
|
var sessQs = 'page=' + currentPage + '&limit=20';
|
|
1537
2353
|
if (filterSearch) sessQs += '&search=' + encodeURIComponent(filterSearch);
|
|
1538
|
-
var
|
|
1539
|
-
if (
|
|
2354
|
+
var tf = getTraceTimeFilter();
|
|
2355
|
+
if (tf.timeFrom) sessQs += '&timeFrom=' + encodeURIComponent(tf.timeFrom);
|
|
2356
|
+
if (tf.timeTo) sessQs += '&timeTo=' + encodeURIComponent(tf.timeTo);
|
|
2357
|
+
if (filterAgentType) sessQs += '&agentType=' + encodeURIComponent(filterAgentType);
|
|
2358
|
+
var statsQs = '';
|
|
2359
|
+
if (tf.timeFrom) statsQs += (statsQs ? '&' : '') + 'timeFrom=' + encodeURIComponent(tf.timeFrom);
|
|
2360
|
+
if (tf.timeTo) statsQs += (statsQs ? '&' : '') + 'timeTo=' + encodeURIComponent(tf.timeTo);
|
|
2361
|
+
if (filterAgentType) statsQs += (statsQs ? '&' : '') + 'agentType=' + encodeURIComponent(filterAgentType);
|
|
1540
2362
|
|
|
1541
2363
|
var data = await Promise.all([
|
|
1542
|
-
fetchApi('/stats'),
|
|
2364
|
+
fetchApi('/stats' + (statsQs ? ('?' + statsQs) : '')),
|
|
1543
2365
|
fetchApi('/sessions?' + sessQs)
|
|
1544
2366
|
]);
|
|
1545
2367
|
var stats = data[0];
|
|
@@ -1549,22 +2371,36 @@ async function renderTraceList() {
|
|
|
1549
2371
|
|
|
1550
2372
|
// Stats cards
|
|
1551
2373
|
html += '<div class="stat-grid">';
|
|
1552
|
-
html += statCard('
|
|
1553
|
-
html += statCard('
|
|
1554
|
-
html += statCard('
|
|
1555
|
-
html += statCard('
|
|
1556
|
-
html += statCard('
|
|
2374
|
+
html += statCard(t('sessions'), fmtNum(stats.totalSessions));
|
|
2375
|
+
html += statCard(t('actions'), fmtNum(stats.totalActions));
|
|
2376
|
+
html += statCard(t('tokens'), fmtNum(stats.totalTokens));
|
|
2377
|
+
html += statCard(t('avg_latency'), fmtDur(stats.avgLatencyMs));
|
|
2378
|
+
html += statCard(t('success'), stats.successRate + '%');
|
|
1557
2379
|
html += '</div>';
|
|
1558
2380
|
|
|
1559
2381
|
// Filter bar
|
|
1560
|
-
var hasFilter = filterSearch || filterTimeRange;
|
|
2382
|
+
var hasFilter = filterSearch || filterTimeRange || (filterAgentType !== 'main');
|
|
1561
2383
|
html += '<div class="filter-bar">';
|
|
1562
|
-
html += '<input type="text" id="f-search" placeholder="
|
|
2384
|
+
html += '<input type="text" id="f-search" placeholder="' + esc(t('search_session')) + '" value="' + esc(filterSearch) + '" onkeydown="if(event.key===\\'Enter\\')applyFilter()">';
|
|
2385
|
+
var agentTypeLabel = filterAgentType === 'subagent'
|
|
2386
|
+
? t('subagent')
|
|
2387
|
+
: (filterAgentType === 'replay'
|
|
2388
|
+
? t('replay')
|
|
2389
|
+
: (filterAgentType === 'system'
|
|
2390
|
+
? t('system')
|
|
2391
|
+
: (filterAgentType === 'all' ? t('all') : t('main'))));
|
|
2392
|
+
html += '<div class="agent-type-dropdown">';
|
|
2393
|
+
html += '<button class="time-btn" onclick="toggleAgentTypeMenu(event)" aria-label="' + esc(t('filter_agent_type')) + '">' + esc(agentTypeLabel) + ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg></button>';
|
|
2394
|
+
html += '<div class="time-menu agent-type-menu" id="agent-type-menu">';
|
|
2395
|
+
html += '<div class="time-menu-item' + (filterAgentType === 'main' ? ' active' : '') + '" onclick="selectAgentTypeMenu(\\'main\\')"><span class="check">' + (filterAgentType === 'main' ? '✓' : '') + '</span>' + esc(t('main')) + '</div>';
|
|
2396
|
+
html += '<div class="time-menu-item' + (filterAgentType === 'subagent' ? ' active' : '') + '" onclick="selectAgentTypeMenu(\\'subagent\\')"><span class="check">' + (filterAgentType === 'subagent' ? '✓' : '') + '</span>' + esc(t('subagent')) + '</div>';
|
|
2397
|
+
html += '<div class="time-menu-item' + (filterAgentType === 'system' ? ' active' : '') + '" onclick="selectAgentTypeMenu(\\'system\\')"><span class="check">' + (filterAgentType === 'system' ? '✓' : '') + '</span>' + esc(t('system')) + '</div>';
|
|
2398
|
+
html += '<div class="time-menu-item' + (filterAgentType === 'replay' ? ' active' : '') + '" onclick="selectAgentTypeMenu(\\'replay\\')"><span class="check">' + (filterAgentType === 'replay' ? '✓' : '') + '</span>' + esc(t('replay')) + '</div>';
|
|
2399
|
+
html += '<div class="time-menu-item' + (filterAgentType === 'all' ? ' active' : '') + '" onclick="selectAgentTypeMenu(\\'all\\')"><span class="check">' + (filterAgentType === 'all' ? '✓' : '') + '</span>' + esc(t('all')) + '</div>';
|
|
2400
|
+
html += '</div></div>';
|
|
1563
2401
|
html += '<span class="filter-sep"></span>';
|
|
1564
|
-
// Time range dropdown
|
|
1565
2402
|
html += '<div class="time-dropdown" id="time-dropdown">';
|
|
1566
2403
|
html += '<button class="time-btn" onclick="toggleTimeMenu(event)">';
|
|
1567
|
-
html += '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
1568
2404
|
html += esc(getTimeLabel());
|
|
1569
2405
|
html += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
1570
2406
|
html += '</button>';
|
|
@@ -1573,22 +2409,22 @@ async function renderTraceList() {
|
|
|
1573
2409
|
var cls = (p.key === filterTimeRange) ? ' active' : '';
|
|
1574
2410
|
html += '<div class="time-menu-item' + cls + '" onclick="selectTimeRange(\\'' + p.key + '\\')">';
|
|
1575
2411
|
html += '<span class="check">' + (p.key === filterTimeRange ? '✓' : '') + '</span>';
|
|
1576
|
-
html += esc(p
|
|
2412
|
+
html += esc(timePresetLabel(p));
|
|
1577
2413
|
html += '</div>';
|
|
1578
2414
|
});
|
|
1579
2415
|
html += '</div></div>';
|
|
1580
|
-
html += '<button class="icon-refresh-btn" onclick="refreshTraceList()" title="
|
|
2416
|
+
html += '<button class="icon-refresh-btn" onclick="refreshTraceList()" title="' + esc(t('refresh_trace_list')) + '">' + ICON_REFRESH + '</button>';
|
|
1581
2417
|
if (hasFilter) {
|
|
1582
|
-
html += '<button class="btn-clear" onclick="clearFilter()">✕
|
|
2418
|
+
html += '<button class="btn-clear" onclick="clearFilter()">✕ ' + esc(t('clear')) + '</button>';
|
|
1583
2419
|
}
|
|
1584
2420
|
html += '</div>';
|
|
1585
2421
|
|
|
1586
2422
|
// Session list
|
|
1587
|
-
html += '<div class="section-title">
|
|
2423
|
+
html += '<div class="section-title">' + esc(t('traces')) + ' <span class="count">' + sessData.total + (hasFilter ? (' ' + esc(t('matched'))) : (' ' + esc(t('total'))) ) + '</span></div>';
|
|
1588
2424
|
html += '<div class="session-list">';
|
|
1589
2425
|
|
|
1590
2426
|
if (sessData.sessions.length === 0) {
|
|
1591
|
-
html += '<div class="empty"><div class="icon">📭</div><div class="text">
|
|
2427
|
+
html += '<div class="empty"><div class="icon">📭</div><div class="text">' + esc(t('no_sessions')) + '</div></div>';
|
|
1592
2428
|
} else {
|
|
1593
2429
|
var visibleSessions = (sessData.sessions || []).filter(function(s) {
|
|
1594
2430
|
var sid = String((s && s.session_id) || '').toLowerCase();
|
|
@@ -1598,24 +2434,31 @@ async function renderTraceList() {
|
|
|
1598
2434
|
var dur = (s.start_time && s.end_time)
|
|
1599
2435
|
? fmtDur(new Date(s.end_time).getTime() - new Date(s.start_time).getTime())
|
|
1600
2436
|
: '-';
|
|
2437
|
+
var parentSessionId = String((s && s.parent_session_id) || '').trim();
|
|
2438
|
+
var userId = String((s && s.user_id) || '');
|
|
2439
|
+
var isSubagent = !!Number((s && s.is_subagent) || 0) || !!parentSessionId || /(^|[:_\\-])subagent([:_\\-]|$)/i.test(userId);
|
|
1601
2440
|
|
|
1602
2441
|
html += '<div class="session-card" onclick="location.hash=\\'#/trace/' + encodeURIComponent(s.session_id) + '\\'">';
|
|
1603
2442
|
html += '<div class="session-top">';
|
|
1604
2443
|
html += '<span class="session-id">' + esc(s.session_id) + '</span>';
|
|
1605
2444
|
html += '<span class="session-model">' + esc(s.model_name || '-') + '</span>';
|
|
2445
|
+
if (isSubagent) {
|
|
2446
|
+
var roleTitle = parentSessionId ? (t('parent') + ': ' + parentSessionId) : t('heuristic_subagent_session');
|
|
2447
|
+
html += '<span class="session-role subagent" title="' + esc(roleTitle) + '">' + esc(t('subagent')) + '</span>';
|
|
2448
|
+
}
|
|
1606
2449
|
html += '<span class="session-user">🤖 ' + esc(s.user_id || '-') + '</span>';
|
|
1607
2450
|
if (s.channel_id) html += '<span class="session-channel">📱 ' + esc(s.channel_id) + '</span>';
|
|
1608
2451
|
html += '<span class="session-time">' + fmtTime(s.start_time) + '</span>';
|
|
1609
2452
|
html += '</div>';
|
|
1610
2453
|
html += '<div class="session-bottom">';
|
|
1611
|
-
html += '<span class="session-stat"><b>' + s.total_actions + '</b>
|
|
1612
|
-
html += '<span class="session-stat"><b>' + fmtNum(s.total_tokens) + '</b>
|
|
2454
|
+
html += '<span class="session-stat"><b>' + s.total_actions + '</b> ' + esc(t('actions_unit')) + '</span>';
|
|
2455
|
+
html += '<span class="session-stat"><b>' + fmtNum(s.total_tokens) + '</b> ' + esc(t('tokens_unit')) + '</span>';
|
|
1613
2456
|
html += '<span class="session-stat"><b>' + dur + '</b></span>';
|
|
1614
2457
|
html += '</div>';
|
|
1615
2458
|
html += '</div>';
|
|
1616
2459
|
});
|
|
1617
2460
|
if (visibleSessions.length === 0) {
|
|
1618
|
-
html += '<div class="empty"><div class="icon">📭</div><div class="text">
|
|
2461
|
+
html += '<div class="empty"><div class="icon">📭</div><div class="text">' + esc(t('no_sessions')) + '</div></div>';
|
|
1619
2462
|
}
|
|
1620
2463
|
}
|
|
1621
2464
|
|
|
@@ -1624,16 +2467,16 @@ async function renderTraceList() {
|
|
|
1624
2467
|
// Pagination
|
|
1625
2468
|
var totalPages = Math.ceil(sessData.total / 20) || 1;
|
|
1626
2469
|
html += '<div class="pagination">';
|
|
1627
|
-
html += '<button onclick="goPage(' + (currentPage-1) + ')" ' + (currentPage<=1?'disabled':'') + '>«
|
|
1628
|
-
html += '<span class="page-info">
|
|
1629
|
-
html += '<button onclick="goPage(' + (currentPage+1) + ')" ' + (currentPage>=totalPages?'disabled':'') + '>
|
|
2470
|
+
html += '<button onclick="goPage(' + (currentPage-1) + ')" ' + (currentPage<=1?'disabled':'') + '>« ' + esc(t('prev')) + '</button>';
|
|
2471
|
+
html += '<span class="page-info">' + esc(t('page')) + ' ' + currentPage + ' / ' + totalPages + '</span>';
|
|
2472
|
+
html += '<button onclick="goPage(' + (currentPage+1) + ')" ' + (currentPage>=totalPages?'disabled':'') + '>' + esc(t('next')) + ' »</button>';
|
|
1630
2473
|
html += '</div>';
|
|
1631
2474
|
|
|
1632
2475
|
app.innerHTML = renderLayout('traces', html);
|
|
1633
2476
|
|
|
1634
2477
|
} catch(err) {
|
|
1635
2478
|
app.innerHTML = renderLayout('traces',
|
|
1636
|
-
'<div class="empty"><div class="icon">⚠️</div><div class="text">
|
|
2479
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>');
|
|
1637
2480
|
}
|
|
1638
2481
|
}
|
|
1639
2482
|
|
|
@@ -1657,6 +2500,9 @@ window.applyFilter = function() {
|
|
|
1657
2500
|
window.clearFilter = function() {
|
|
1658
2501
|
filterSearch = '';
|
|
1659
2502
|
filterTimeRange = '';
|
|
2503
|
+
filterTimeFrom = '';
|
|
2504
|
+
filterTimeTo = '';
|
|
2505
|
+
filterAgentType = 'main';
|
|
1660
2506
|
currentPage = 1;
|
|
1661
2507
|
renderTraceList();
|
|
1662
2508
|
};
|
|
@@ -1673,16 +2519,100 @@ window.toggleTimeMenu = function(e) {
|
|
|
1673
2519
|
|
|
1674
2520
|
window.selectTimeRange = function(key) {
|
|
1675
2521
|
filterTimeRange = key;
|
|
2522
|
+
var nowIso = new Date().toISOString();
|
|
2523
|
+
if (!key) {
|
|
2524
|
+
filterTimeFrom = '';
|
|
2525
|
+
filterTimeTo = '';
|
|
2526
|
+
} else {
|
|
2527
|
+
var preset = TIME_PRESETS.find(function(p) { return p.key === key; });
|
|
2528
|
+
if (preset && preset.ms) {
|
|
2529
|
+
filterTimeFrom = new Date(Date.now() - preset.ms).toISOString();
|
|
2530
|
+
filterTimeTo = nowIso;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
1676
2533
|
currentPage = 1;
|
|
1677
2534
|
var menu = document.getElementById('time-menu');
|
|
1678
2535
|
if (menu) menu.classList.remove('open');
|
|
1679
2536
|
renderTraceList();
|
|
1680
2537
|
};
|
|
1681
2538
|
|
|
2539
|
+
window.selectAgentType = function(value) {
|
|
2540
|
+
filterAgentType = (value === 'subagent' || value === 'system' || value === 'replay' || value === 'all') ? value : 'main';
|
|
2541
|
+
currentPage = 1;
|
|
2542
|
+
renderTraceList();
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
window.toggleAgentTypeMenu = function(e) {
|
|
2546
|
+
e.stopPropagation();
|
|
2547
|
+
var menu = document.getElementById('agent-type-menu');
|
|
2548
|
+
if (!menu) return;
|
|
2549
|
+
var isOpen = menu.classList.contains('open');
|
|
2550
|
+
menu.classList.remove('open');
|
|
2551
|
+
if (!isOpen) menu.classList.add('open');
|
|
2552
|
+
};
|
|
2553
|
+
|
|
2554
|
+
window.selectAgentTypeMenu = function(value) {
|
|
2555
|
+
var menu = document.getElementById('agent-type-menu');
|
|
2556
|
+
if (menu) menu.classList.remove('open');
|
|
2557
|
+
window.selectAgentType(value);
|
|
2558
|
+
};
|
|
2559
|
+
|
|
2560
|
+
window.selectTenantScope = function(scopeId) {
|
|
2561
|
+
selectedScopeId = String(scopeId || 'local');
|
|
2562
|
+
saveScopeId(selectedScopeId);
|
|
2563
|
+
currentPage = 1;
|
|
2564
|
+
router();
|
|
2565
|
+
};
|
|
2566
|
+
|
|
2567
|
+
function closeTenantMenus() {
|
|
2568
|
+
['tenant-project-menu'].forEach(function(id) {
|
|
2569
|
+
var el = document.getElementById(id);
|
|
2570
|
+
if (el) el.classList.remove('open');
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
function closeLanguageMenu() {
|
|
2574
|
+
var el = document.getElementById('lang-menu');
|
|
2575
|
+
if (el) el.classList.remove('open');
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
window.toggleTenantMenu = function(e, key) {
|
|
2579
|
+
e.stopPropagation();
|
|
2580
|
+
var id = key === 'project' ? 'tenant-project-menu' : '';
|
|
2581
|
+
var menu = document.getElementById(id);
|
|
2582
|
+
closeTenantMenus();
|
|
2583
|
+
if (menu) menu.classList.toggle('open');
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
window.selectTenantScopeMenu = function(value) {
|
|
2587
|
+
closeTenantMenus();
|
|
2588
|
+
window.selectTenantScope(value);
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
window.toggleLanguageMenu = function(e) {
|
|
2592
|
+
e.stopPropagation();
|
|
2593
|
+
var menu = document.getElementById('lang-menu');
|
|
2594
|
+
closeLanguageMenu();
|
|
2595
|
+
if (menu) menu.classList.toggle('open');
|
|
2596
|
+
};
|
|
2597
|
+
window.selectLanguageMenu = function(value) {
|
|
2598
|
+
closeLanguageMenu();
|
|
2599
|
+
currentLang = normalizeLang(value);
|
|
2600
|
+
try { window.localStorage.setItem(LANG_STORAGE_KEY, currentLang); } catch (_) {}
|
|
2601
|
+
router();
|
|
2602
|
+
};
|
|
2603
|
+
|
|
1682
2604
|
// Click anywhere on page to close dropdowns
|
|
1683
2605
|
document.addEventListener('click', function() {
|
|
1684
2606
|
var menu = document.getElementById('time-menu');
|
|
1685
2607
|
if (menu) menu.classList.remove('open');
|
|
2608
|
+
var metricsMenu = document.getElementById('metrics-time-menu');
|
|
2609
|
+
if (metricsMenu) metricsMenu.classList.remove('open');
|
|
2610
|
+
var skillsMenu = document.getElementById('skills-time-menu');
|
|
2611
|
+
if (skillsMenu) skillsMenu.classList.remove('open');
|
|
2612
|
+
var agentMenu = document.getElementById('agent-type-menu');
|
|
2613
|
+
if (agentMenu) agentMenu.classList.remove('open');
|
|
2614
|
+
closeTenantMenus();
|
|
2615
|
+
closeLanguageMenu();
|
|
1686
2616
|
// Close analytics dropdowns too
|
|
1687
2617
|
var anMenu = document.getElementById('an-time-menu');
|
|
1688
2618
|
if (anMenu) anMenu.classList.remove('open');
|
|
@@ -1696,6 +2626,12 @@ var anTimeRange = '24h';
|
|
|
1696
2626
|
var anMetricTab = 'sessions'; // sessions | tokens
|
|
1697
2627
|
var __dashboardCache = {};
|
|
1698
2628
|
|
|
2629
|
+
var skillsTimeRange = '24h';
|
|
2630
|
+
var skillsLimit = 200;
|
|
2631
|
+
var skillsSelected = '';
|
|
2632
|
+
var skillsOpenEncoded = '';
|
|
2633
|
+
var skillsDetailCache = {};
|
|
2634
|
+
|
|
1699
2635
|
function anGetTimeFromISO() {
|
|
1700
2636
|
if (!anTimeRange) return '';
|
|
1701
2637
|
var preset = TIME_PRESETS.find(function(p){ return p.key === anTimeRange; });
|
|
@@ -1704,9 +2640,9 @@ function anGetTimeFromISO() {
|
|
|
1704
2640
|
}
|
|
1705
2641
|
|
|
1706
2642
|
function anGetTimeLabel() {
|
|
1707
|
-
if (!anTimeRange) return 'All time';
|
|
2643
|
+
if (!anTimeRange) return currentLang === 'zh' ? '全部时间' : 'All time';
|
|
1708
2644
|
var preset = TIME_PRESETS.find(function(p){ return p.key === anTimeRange; });
|
|
1709
|
-
return preset ? preset
|
|
2645
|
+
return preset ? timePresetLabel(preset) : (currentLang === 'zh' ? '全部时间' : 'All time');
|
|
1710
2646
|
}
|
|
1711
2647
|
|
|
1712
2648
|
async function renderDashboard(opts) {
|
|
@@ -1714,7 +2650,7 @@ async function renderDashboard(opts) {
|
|
|
1714
2650
|
var cacheKey = anTimeRange || 'all';
|
|
1715
2651
|
var cached = __dashboardCache[cacheKey];
|
|
1716
2652
|
if (!cached || opts.forceReload) {
|
|
1717
|
-
app.innerHTML = renderLayout('dashboard', '<div class="loading">
|
|
2653
|
+
app.innerHTML = renderLayout('dashboard', '<div class="loading">' + esc(t('loading_dashboard')) + '</div>');
|
|
1718
2654
|
}
|
|
1719
2655
|
|
|
1720
2656
|
try {
|
|
@@ -1749,7 +2685,7 @@ async function renderDashboard(opts) {
|
|
|
1749
2685
|
var cls = (p.key === anTimeRange) ? ' active' : '';
|
|
1750
2686
|
html += '<div class="time-menu-item' + cls + '" onclick="anSelectTime(\\'' + p.key + '\\')">';
|
|
1751
2687
|
html += '<span class="check">' + (p.key === anTimeRange ? '✓' : '') + '</span>';
|
|
1752
|
-
html += esc(p
|
|
2688
|
+
html += esc(timePresetLabel(p));
|
|
1753
2689
|
html += '</div>';
|
|
1754
2690
|
});
|
|
1755
2691
|
html += '</div></div>';
|
|
@@ -1758,19 +2694,19 @@ async function renderDashboard(opts) {
|
|
|
1758
2694
|
// --- KPI stat cards ---
|
|
1759
2695
|
var inpPct = ov.totalTokens > 0 ? Math.round(ov.inputTokens / ov.totalTokens * 100) : 50;
|
|
1760
2696
|
html += '<div class="stat-grid" style="grid-template-columns:repeat(auto-fit,minmax(140px,1fr))">';
|
|
1761
|
-
html += statCard('Sessions', fmtNum(ov.totalSessions));
|
|
2697
|
+
html += statCard(tr('Sessions', '会话数'), fmtNum(ov.totalSessions));
|
|
1762
2698
|
|
|
1763
2699
|
// Tokens with input/output split
|
|
1764
|
-
html += '<div class="stat"><div class="stat-label">Total Tokens</div>';
|
|
2700
|
+
html += '<div class="stat"><div class="stat-label">' + esc(tr('Total Tokens', '总 Token')) + '</div>';
|
|
1765
2701
|
html += '<div class="stat-value">' + fmtNum(ov.totalTokens) + '</div>';
|
|
1766
2702
|
html += '<div class="token-split"><div class="inp" style="width:' + inpPct + '%"></div><div class="outp" style="width:' + (100 - inpPct) + '%"></div></div>';
|
|
1767
|
-
html += '<div class="kpi-sub"><span class="inp-color">⬤</span> ' + fmtNum(ov.inputTokens) + ' in <span class="outp-color">⬤</span> ' + fmtNum(ov.outputTokens) + ' out</div>';
|
|
2703
|
+
html += '<div class="kpi-sub"><span class="inp-color">⬤</span> ' + fmtNum(ov.inputTokens) + ' ' + esc(tr('in', '输入')) + ' <span class="outp-color">⬤</span> ' + fmtNum(ov.outputTokens) + ' ' + esc(tr('out', '输出')) + '</div>';
|
|
1768
2704
|
html += '</div>';
|
|
1769
2705
|
|
|
1770
|
-
html += statCard('Actions', fmtNum(ov.totalActions));
|
|
1771
|
-
html += statCard('Avg Latency', fmtDur(ov.avgLatencyMs));
|
|
1772
|
-
html += statCard('Models', String(ov.activeModels));
|
|
1773
|
-
html += statCard('Alerts', String(ov.securityAlerts));
|
|
2706
|
+
html += statCard(tr('Actions', '动作数'), fmtNum(ov.totalActions));
|
|
2707
|
+
html += statCard(tr('Avg Latency', '平均耗时'), fmtDur(ov.avgLatencyMs));
|
|
2708
|
+
html += statCard(tr('Models', '模型数'), String(ov.activeModels));
|
|
2709
|
+
html += statCard(tr('Alerts', '告警数'), String(ov.securityAlerts));
|
|
1774
2710
|
html += '</div>';
|
|
1775
2711
|
|
|
1776
2712
|
// --- Row 1: Traces by time + Token usage by time ---
|
|
@@ -1780,15 +2716,15 @@ async function renderDashboard(opts) {
|
|
|
1780
2716
|
html += '<div class="an-card">';
|
|
1781
2717
|
var gran = data.granularity || 'day';
|
|
1782
2718
|
var granLabel = gran === 'hour' ? anGetTimeLabel() : (ts.length + ' days');
|
|
1783
|
-
html += '<h3><span class="icon">📊</span> Activity Over Time';
|
|
2719
|
+
html += '<h3><span class="icon">📊</span> ' + esc(tr('Activity Over Time', '时间趋势')) ;
|
|
1784
2720
|
html += '<span class="sub">' + granLabel + '</span></h3>';
|
|
1785
2721
|
html += buildTimeSeriesChart(ts, anMetricTab, gran);
|
|
1786
2722
|
html += '</div>';
|
|
1787
2723
|
|
|
1788
2724
|
// Model Usage (horizontal bars)
|
|
1789
2725
|
html += '<div class="an-card">';
|
|
1790
|
-
html += '<h3><span class="icon">🤖</span> Model Usage';
|
|
1791
|
-
html += '<span class="sub">' + mu.length + ' models</span></h3>';
|
|
2726
|
+
html += '<h3><span class="icon">🤖</span> ' + esc(tr('Model Usage', '模型使用')) ;
|
|
2727
|
+
html += '<span class="sub">' + mu.length + ' ' + esc(tr('models', '个模型')) + '</span></h3>';
|
|
1792
2728
|
html += buildModelUsageChart(mu, tbm);
|
|
1793
2729
|
html += '</div>';
|
|
1794
2730
|
|
|
@@ -1799,14 +2735,14 @@ async function renderDashboard(opts) {
|
|
|
1799
2735
|
|
|
1800
2736
|
// Action type distribution (donut + legend)
|
|
1801
2737
|
html += '<div class="an-card">';
|
|
1802
|
-
html += '<h3><span class="icon">🎯</span> Action Distribution</h3>';
|
|
2738
|
+
html += '<h3><span class="icon">🎯</span> ' + esc(tr('Action Distribution', '动作分布')) + '</h3>';
|
|
1803
2739
|
html += buildActionDistribution(ad);
|
|
1804
2740
|
html += '</div>';
|
|
1805
2741
|
|
|
1806
2742
|
// Top agents table
|
|
1807
2743
|
html += '<div class="an-card">';
|
|
1808
|
-
html += '<h3><span class="icon">👤</span> Top Agents';
|
|
1809
|
-
html += '<span class="sub">' + ta.length + ' agents</span></h3>';
|
|
2744
|
+
html += '<h3><span class="icon">👤</span> ' + esc(tr('Top Agents', '高频代理')) ;
|
|
2745
|
+
html += '<span class="sub">' + ta.length + ' ' + esc(tr('agents', '个代理')) + '</span></h3>';
|
|
1810
2746
|
html += buildAgentsTable(ta);
|
|
1811
2747
|
html += '</div>';
|
|
1812
2748
|
|
|
@@ -1816,59 +2752,367 @@ async function renderDashboard(opts) {
|
|
|
1816
2752
|
|
|
1817
2753
|
} catch(err) {
|
|
1818
2754
|
app.innerHTML = renderLayout('dashboard',
|
|
1819
|
-
'<div class="empty"><div class="icon">⚠️</div><div class="text">
|
|
2755
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>');
|
|
1820
2756
|
}
|
|
1821
2757
|
}
|
|
1822
2758
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
var n = Number(value);
|
|
1829
|
-
metricsRangeMinutes = Number.isFinite(n) && n > 0 ? n : 60;
|
|
1830
|
-
renderMetrics();
|
|
2759
|
+
function skillsGetTimeFromISO() {
|
|
2760
|
+
if (!skillsTimeRange) return '';
|
|
2761
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === skillsTimeRange; });
|
|
2762
|
+
if (!preset || !preset.ms) return '';
|
|
2763
|
+
return new Date(Date.now() - preset.ms).toISOString();
|
|
1831
2764
|
}
|
|
1832
2765
|
|
|
1833
|
-
function
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
renderMetrics();
|
|
2766
|
+
function skillsGetTimeToISO() {
|
|
2767
|
+
if (!skillsTimeRange) return '';
|
|
2768
|
+
return new Date().toISOString();
|
|
1837
2769
|
}
|
|
1838
2770
|
|
|
1839
|
-
function
|
|
1840
|
-
|
|
1841
|
-
|
|
2771
|
+
function skillsGetTimeLabel() {
|
|
2772
|
+
if (!skillsTimeRange) return currentLang === 'zh' ? '全部时间' : 'All time';
|
|
2773
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === skillsTimeRange; });
|
|
2774
|
+
return preset ? timePresetLabel(preset) : (currentLang === 'zh' ? '全部时间' : 'All time');
|
|
1842
2775
|
}
|
|
1843
2776
|
|
|
1844
|
-
function
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2777
|
+
window.skillsToggleTimeMenu = function(e) {
|
|
2778
|
+
e.stopPropagation();
|
|
2779
|
+
var menu = document.getElementById('skills-time-menu');
|
|
2780
|
+
if (!menu) return;
|
|
2781
|
+
var isOpen = menu.classList.contains('open');
|
|
2782
|
+
menu.classList.remove('open');
|
|
2783
|
+
if (!isOpen) menu.classList.add('open');
|
|
2784
|
+
};
|
|
2785
|
+
|
|
2786
|
+
window.skillsSelectTime = function(key) {
|
|
2787
|
+
skillsTimeRange = key;
|
|
2788
|
+
var menu = document.getElementById('skills-time-menu');
|
|
2789
|
+
if (menu) menu.classList.remove('open');
|
|
2790
|
+
renderSkills();
|
|
2791
|
+
};
|
|
2792
|
+
|
|
2793
|
+
window.skillsSetLimit = function(v) {
|
|
2794
|
+
var n = Number(v);
|
|
2795
|
+
skillsLimit = Number.isFinite(n) && n > 0 ? Math.max(10, Math.min(500, Math.floor(n))) : 200;
|
|
2796
|
+
renderSkills();
|
|
2797
|
+
};
|
|
2798
|
+
|
|
2799
|
+
window.refreshSkills = function() {
|
|
2800
|
+
renderSkills();
|
|
2801
|
+
};
|
|
2802
|
+
|
|
2803
|
+
window.skillsOpenDetail = function(name) {
|
|
2804
|
+
skillsSelected = String(name || '').trim();
|
|
2805
|
+
skillsOpenEncoded = encodeURIComponent(skillsSelected);
|
|
2806
|
+
renderSkills();
|
|
2807
|
+
};
|
|
2808
|
+
|
|
2809
|
+
window.skillsOpenDetailByEncoded = function(encoded) {
|
|
1849
2810
|
try {
|
|
1850
|
-
|
|
1851
|
-
} catch (
|
|
1852
|
-
|
|
2811
|
+
skillsSelected = decodeURIComponent(String(encoded || ''));
|
|
2812
|
+
} catch (_) {
|
|
2813
|
+
skillsSelected = String(encoded || '');
|
|
1853
2814
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2815
|
+
skillsSelected = String(skillsSelected || '').trim();
|
|
2816
|
+
skillsOpenEncoded = encodeURIComponent(skillsSelected);
|
|
2817
|
+
renderSkills();
|
|
2818
|
+
};
|
|
2819
|
+
|
|
2820
|
+
function skillsBuildDetailHtml(detail, detailError) {
|
|
2821
|
+
if (!detail || !detail.skill) {
|
|
2822
|
+
return '<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(detailError || 'detail unavailable') + '</div></div>';
|
|
2823
|
+
}
|
|
2824
|
+
var calls = Array.isArray(detail.recentCalls) ? detail.recentCalls : [];
|
|
2825
|
+
var html = '';
|
|
2826
|
+
html += '<div class="kpi-sub" style="margin-bottom:8px"><strong>' + esc(t('skill_description')) + ':</strong> ' + esc(String(detail.description || '-')) + '</div>';
|
|
2827
|
+
html += '<div class="kpi-sub" style="margin-bottom:8px"><strong>' + esc(t('skill_location')) + ':</strong> <span class="mono">' + esc(String(detail.location || '-')) + '</span></div>';
|
|
2828
|
+
html += '<div class="kpi-sub" style="margin-bottom:8px"><strong>Source:</strong> <span class="mono">' + esc(String(detail.source || '-')) + '</span></div>';
|
|
2829
|
+
html += '<div style="margin:8px 0 6px;font-weight:600">' + esc(t('skill_content')) + '</div>';
|
|
2830
|
+
if (detail.contentLoaded && typeof detail.content === 'string' && detail.content.length > 0) {
|
|
2831
|
+
html += '<pre class="json-view" style="max-height:260px;overflow:auto">' + esc(String(detail.content)) + '</pre>';
|
|
2832
|
+
} else {
|
|
2833
|
+
html += '<div class="kpi-sub">-</div>';
|
|
2834
|
+
}
|
|
2835
|
+
html += '<div style="margin:12px 0 6px;font-weight:600">' + esc(t('recent_calls')) + '</div>';
|
|
2836
|
+
if (!calls.length) {
|
|
2837
|
+
html += '<div class="kpi-sub">' + esc(t('no_recent_calls')) + '</div>';
|
|
2838
|
+
} else {
|
|
2839
|
+
html += '<div class="an-table-scroll"><table class="an-table"><thead><tr>';
|
|
2840
|
+
html += '<th>' + esc(t('time')) + '</th><th>Session</th><th>' + esc(t('status')) + '</th><th>ToolCall</th><th>Source</th>';
|
|
2841
|
+
html += '</tr></thead><tbody>';
|
|
2842
|
+
calls.forEach(function(c) {
|
|
2843
|
+
var ok = c.ok === true;
|
|
2844
|
+
var fail = c.ok === false;
|
|
2845
|
+
var traceLink = '#/trace/' + encodeURIComponent(String(c.sessionId || ''));
|
|
2846
|
+
if (detail.skill) {
|
|
2847
|
+
traceLink += '?action=' + encodeURIComponent('skill_call:' + String(detail.skill));
|
|
2848
|
+
}
|
|
2849
|
+
if (c.createdAt) {
|
|
2850
|
+
traceLink += (traceLink.indexOf('?') >= 0 ? '&' : '?') + 't=' + encodeURIComponent(String(c.createdAt));
|
|
2851
|
+
}
|
|
2852
|
+
html += '<tr>';
|
|
2853
|
+
html += '<td class="mono">' + esc(c.createdAt ? fmtTime(c.createdAt) : '-') + '</td>';
|
|
2854
|
+
html += '<td class="mono">' + (c.sessionId ? '<a href="' + esc(traceLink) + '">' + esc(String(c.sessionId || '-')) + '</a>' : '-') + '</td>';
|
|
2855
|
+
html += '<td>' + (ok ? '<span style="color:var(--ok)">' + esc(t('success_short')) + '</span>' : (fail ? '<span style="color:var(--danger)">' + esc(t('fail_short')) + '</span>' : '-')) + '</td>';
|
|
2856
|
+
html += '<td class="mono">' + esc(String(c.toolCallId || '-')) + '</td>';
|
|
2857
|
+
html += '<td class="mono">' + esc(String(c.skillSource || '-')) + '</td>';
|
|
2858
|
+
html += '</tr>';
|
|
1858
2859
|
});
|
|
1859
|
-
|
|
2860
|
+
html += '</tbody></table></div>';
|
|
2861
|
+
}
|
|
2862
|
+
return html;
|
|
1860
2863
|
}
|
|
1861
2864
|
|
|
1862
|
-
function
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2865
|
+
window.skillsToggleDetailByEncoded = async function(encoded, forceOpen) {
|
|
2866
|
+
var name = '';
|
|
2867
|
+
try {
|
|
2868
|
+
name = decodeURIComponent(String(encoded || ''));
|
|
2869
|
+
} catch (_) {
|
|
2870
|
+
name = String(encoded || '');
|
|
2871
|
+
}
|
|
2872
|
+
name = String(name || '').trim();
|
|
2873
|
+
if (!name) return;
|
|
2874
|
+
var enc = encodeURIComponent(name);
|
|
2875
|
+
function closeCard(targetEnc) {
|
|
2876
|
+
var card = document.getElementById('skills-card-' + targetEnc);
|
|
2877
|
+
var panel = document.getElementById('skills-expand-' + targetEnc);
|
|
2878
|
+
if (card) card.classList.remove('is-active');
|
|
2879
|
+
if (panel) {
|
|
2880
|
+
panel.style.display = 'none';
|
|
2881
|
+
panel.innerHTML = '';
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
if (skillsOpenEncoded && skillsOpenEncoded !== enc) {
|
|
2886
|
+
closeCard(skillsOpenEncoded);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
if (!forceOpen && skillsOpenEncoded === enc) {
|
|
2890
|
+
closeCard(enc);
|
|
2891
|
+
skillsOpenEncoded = '';
|
|
2892
|
+
skillsSelected = '';
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
var cardEl = document.getElementById('skills-card-' + enc);
|
|
2897
|
+
var panelEl = document.getElementById('skills-expand-' + enc);
|
|
2898
|
+
if (!cardEl || !panelEl) {
|
|
2899
|
+
skillsSelected = name;
|
|
2900
|
+
skillsOpenEncoded = enc;
|
|
2901
|
+
renderSkills();
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
cardEl.classList.add('is-active');
|
|
2906
|
+
panelEl.style.display = 'block';
|
|
2907
|
+
panelEl.innerHTML = '<div class="loading">' + esc(t('loading')) + '</div>';
|
|
2908
|
+
skillsOpenEncoded = enc;
|
|
2909
|
+
skillsSelected = name;
|
|
2910
|
+
|
|
2911
|
+
var tf = skillsGetTimeFromISO();
|
|
2912
|
+
var tt = skillsGetTimeToISO();
|
|
2913
|
+
var cacheKey = enc + '|' + (tf || '') + '|' + (tt || '');
|
|
2914
|
+
if (skillsDetailCache[cacheKey]) {
|
|
2915
|
+
panelEl.innerHTML = skillsBuildDetailHtml(skillsDetailCache[cacheKey], '');
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
try {
|
|
2919
|
+
var detail = await fetchApi('/skills/detail?skill=' + encodeURIComponent(name) + '&limit=30' + (tf ? '&timeFrom=' + encodeURIComponent(tf) : '') + (tt ? '&timeTo=' + encodeURIComponent(tt) : ''));
|
|
2920
|
+
skillsDetailCache[cacheKey] = detail;
|
|
2921
|
+
panelEl.innerHTML = skillsBuildDetailHtml(detail, '');
|
|
2922
|
+
} catch (e) {
|
|
2923
|
+
panelEl.innerHTML = skillsBuildDetailHtml(null, String(e || ''));
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
|
|
2927
|
+
window.skillsCloseDetail = function() {
|
|
2928
|
+
skillsSelected = '';
|
|
2929
|
+
if (skillsOpenEncoded) {
|
|
2930
|
+
var card = document.getElementById('skills-card-' + skillsOpenEncoded);
|
|
2931
|
+
var panel = document.getElementById('skills-expand-' + skillsOpenEncoded);
|
|
2932
|
+
if (card) card.classList.remove('is-active');
|
|
2933
|
+
if (panel) { panel.style.display = 'none'; panel.innerHTML = ''; }
|
|
2934
|
+
skillsOpenEncoded = '';
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
renderSkills();
|
|
2938
|
+
};
|
|
2939
|
+
|
|
2940
|
+
async function renderSkills() {
|
|
2941
|
+
app.innerHTML = renderLayout('skills', '<div class="loading">' + esc(t('loading_skills')) + '</div>');
|
|
2942
|
+
try {
|
|
2943
|
+
var tf = skillsGetTimeFromISO();
|
|
2944
|
+
var tt = skillsGetTimeToISO();
|
|
2945
|
+
var qs = '?limit=' + encodeURIComponent(String(skillsLimit));
|
|
2946
|
+
if (tf) qs += '&timeFrom=' + encodeURIComponent(tf);
|
|
2947
|
+
if (tt) qs += '&timeTo=' + encodeURIComponent(tt);
|
|
2948
|
+
var data = await fetchApi('/skills/overview' + qs);
|
|
2949
|
+
var items = Array.isArray(data && data.items) ? data.items : [];
|
|
2950
|
+
|
|
2951
|
+
var html = '';
|
|
2952
|
+
html += '<div class="filter-bar" style="margin-bottom:20px">';
|
|
2953
|
+
html += '<div class="time-dropdown">';
|
|
2954
|
+
html += '<button class="time-btn" onclick="skillsToggleTimeMenu(event)">';
|
|
2955
|
+
html += esc(skillsGetTimeLabel());
|
|
2956
|
+
html += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
2957
|
+
html += '</button>';
|
|
2958
|
+
html += '<div class="time-menu" id="skills-time-menu">';
|
|
2959
|
+
TIME_PRESETS.forEach(function(p) {
|
|
2960
|
+
var cls = (p.key === skillsTimeRange) ? ' active' : '';
|
|
2961
|
+
html += '<div class="time-menu-item' + cls + '" onclick="skillsSelectTime(\\'' + p.key + '\\')">';
|
|
2962
|
+
html += '<span class="check">' + (p.key === skillsTimeRange ? '✓' : '') + '</span>';
|
|
2963
|
+
html += esc(timePresetLabel(p));
|
|
2964
|
+
html += '</div>';
|
|
2965
|
+
});
|
|
2966
|
+
html += '</div></div>';
|
|
2967
|
+
html += '<select class="filter-select" onchange="skillsSetLimit(this.value)">';
|
|
2968
|
+
[50, 100, 200, 500].forEach(function(n) {
|
|
2969
|
+
html += '<option value="' + n + '"' + (n === skillsLimit ? ' selected' : '') + '>Top ' + n + '</option>';
|
|
2970
|
+
});
|
|
2971
|
+
html += '</select>';
|
|
2972
|
+
html += '<button class="icon-refresh-btn" onclick="refreshSkills()" title="' + esc(t('refresh_trace_list')) + '">' + ICON_REFRESH + '</button>';
|
|
2973
|
+
html += '</div>';
|
|
2974
|
+
|
|
2975
|
+
html += '<div class="stat-grid" style="grid-template-columns:repeat(auto-fit,minmax(160px,1fr))">';
|
|
2976
|
+
html += statCard(t('skills_title'), fmtNum(Number(data.totalSkills || 0)));
|
|
2977
|
+
html += statCard(t('call_count'), fmtNum(Number(data.totalCalls || 0)));
|
|
2978
|
+
html += '</div>';
|
|
2979
|
+
|
|
2980
|
+
html += '<div class="section-title">' + esc(t('skills_title')) + ' <span class="count">' + fmtNum(items.length) + '</span></div>';
|
|
2981
|
+
if (!items.length) {
|
|
2982
|
+
html += '<div class="empty"><div class="icon">🧩</div><div class="text">' + esc(t('no_skills')) + '</div></div>';
|
|
2983
|
+
} else {
|
|
2984
|
+
html += '<div class="session-list skills-list" id="skills-list">';
|
|
2985
|
+
items.forEach(function(it) {
|
|
2986
|
+
var skillName = String(it.skill || '-');
|
|
2987
|
+
var active = skillsSelected === skillName;
|
|
2988
|
+
var encSkill = encodeURIComponent(skillName);
|
|
2989
|
+
html += '<div class="session-card skills-card' + (active ? ' is-active' : '') + '" id="skills-card-' + encSkill + '" onclick="skillsToggleDetailByEncoded(\\'' + encSkill + '\\')">';
|
|
2990
|
+
html += '<div class="session-top">';
|
|
2991
|
+
html += '<span class="session-id">' + esc(skillName) + '</span>';
|
|
2992
|
+
html += '<span class="session-model">' + esc(String(it.source || '-')) + '</span>';
|
|
2993
|
+
html += '<span class="session-model">' + fmtNum(Number(it.callCount || 0)) + ' ' + esc(t('call_count')) + '</span>';
|
|
2994
|
+
html += '<span class="session-time">' + esc(it.lastSeen ? fmtTime(it.lastSeen) : '-') + '</span>';
|
|
2995
|
+
html += '</div>';
|
|
2996
|
+
html += '<div class="skills-expand" id="skills-expand-' + encSkill + '" onclick="event.stopPropagation()"></div>';
|
|
2997
|
+
html += '</div>';
|
|
2998
|
+
});
|
|
2999
|
+
html += '</div>';
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
app.innerHTML = renderLayout('skills', html);
|
|
3003
|
+
if (skillsSelected) {
|
|
3004
|
+
var reopen = encodeURIComponent(skillsSelected);
|
|
3005
|
+
setTimeout(function() { window.skillsToggleDetailByEncoded(reopen, true); }, 0);
|
|
3006
|
+
}
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
app.innerHTML = renderLayout('skills',
|
|
3009
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>');
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/* ================================================================ */
|
|
3014
|
+
/* Metrics (direct scrape from OpenClaw metrics endpoint) */
|
|
3015
|
+
/* ================================================================ */
|
|
3016
|
+
|
|
3017
|
+
function metricsRangeChange(value) {
|
|
3018
|
+
var n = Number(value);
|
|
3019
|
+
metricsRangeMinutes = Number.isFinite(n) && n > 0 ? n : 60;
|
|
3020
|
+
var now = Date.now();
|
|
3021
|
+
metricsTimeTo = new Date(now).toISOString();
|
|
3022
|
+
metricsTimeFrom = new Date(now - metricsRangeMinutes * 60 * 1000).toISOString();
|
|
3023
|
+
renderMetrics();
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
window.metricsToggleRangeMenu = function(e) {
|
|
3027
|
+
e.stopPropagation();
|
|
3028
|
+
var menu = document.getElementById('metrics-time-menu');
|
|
3029
|
+
if (!menu) return;
|
|
3030
|
+
var isOpen = menu.classList.contains('open');
|
|
3031
|
+
menu.classList.remove('open');
|
|
3032
|
+
if (!isOpen) menu.classList.add('open');
|
|
3033
|
+
};
|
|
3034
|
+
|
|
3035
|
+
window.metricsSelectRange = function(value) {
|
|
3036
|
+
var menu = document.getElementById('metrics-time-menu');
|
|
3037
|
+
if (menu) menu.classList.remove('open');
|
|
3038
|
+
metricsRangeChange(value);
|
|
3039
|
+
};
|
|
3040
|
+
|
|
3041
|
+
function metricsStepChange(value) {
|
|
3042
|
+
var n = Number(value);
|
|
3043
|
+
metricsStepSec = Number.isFinite(n) && n >= 5 && n <= 600 ? n : 30;
|
|
3044
|
+
renderMetrics();
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function ensureMetricsTimeRangeDefaults() {
|
|
3048
|
+
if (metricsTimeFrom && metricsTimeTo) return;
|
|
3049
|
+
var now = Date.now();
|
|
3050
|
+
metricsTimeTo = new Date(now).toISOString();
|
|
3051
|
+
metricsTimeFrom = new Date(now - metricsRangeMinutes * 60 * 1000).toISOString();
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
function computeAutoStepSec(startMs, endMs) {
|
|
3055
|
+
var s = Number(startMs || 0);
|
|
3056
|
+
var e = Number(endMs || 0);
|
|
3057
|
+
if (!Number.isFinite(s) || !Number.isFinite(e) || e <= s) return 30;
|
|
3058
|
+
var spanSec = Math.max(1, Math.floor((e - s) / 1000));
|
|
3059
|
+
var targetBuckets = 120;
|
|
3060
|
+
var raw = Math.ceil(spanSec / targetBuckets);
|
|
3061
|
+
var choices = [5, 10, 15, 30, 60, 120, 300, 600];
|
|
3062
|
+
for (var i = 0; i < choices.length; i++) {
|
|
3063
|
+
if (choices[i] >= raw) return choices[i];
|
|
3064
|
+
}
|
|
3065
|
+
return 600;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
function getMetricsTimeFilter() {
|
|
3069
|
+
return {
|
|
3070
|
+
timeFrom: metricsTimeFrom || '',
|
|
3071
|
+
timeTo: metricsTimeTo || '',
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
window.metricsApplyTimeInputs = function() {
|
|
3076
|
+
var fromEl = document.getElementById('m-time-from');
|
|
3077
|
+
var toEl = document.getElementById('m-time-to');
|
|
3078
|
+
metricsTimeFrom = toISOFromDateTimeInputValue(fromEl && fromEl.value ? String(fromEl.value) : '');
|
|
3079
|
+
metricsTimeTo = toISOFromDateTimeInputValue(toEl && toEl.value ? String(toEl.value) : '');
|
|
3080
|
+
renderMetrics();
|
|
3081
|
+
};
|
|
3082
|
+
|
|
3083
|
+
function metricsSortChange(value) {
|
|
3084
|
+
metricsTableSort = value || 'name';
|
|
3085
|
+
renderMetrics();
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
function metricsRowClick(row) {
|
|
3089
|
+
if (!row) return;
|
|
3090
|
+
if (hasActiveTextSelection()) return;
|
|
3091
|
+
var enc = row.getAttribute('data-m');
|
|
3092
|
+
if (!enc) return;
|
|
3093
|
+
try {
|
|
3094
|
+
metricsSelected = decodeURIComponent(enc);
|
|
3095
|
+
} catch (e) {
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
renderMetrics().then(function() {
|
|
3099
|
+
requestAnimationFrame(function() {
|
|
3100
|
+
var el = document.querySelector('[data-spark-metric="' + enc + '"]');
|
|
3101
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
3102
|
+
});
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function metricsTableFilterInput(value) {
|
|
3107
|
+
metricsSnapshotFilter = value == null ? '' : String(value);
|
|
3108
|
+
var tbody = document.getElementById('metrics-snapshot-tbody');
|
|
3109
|
+
if (!tbody) return;
|
|
3110
|
+
var needle = metricsSnapshotFilter.trim().toLowerCase();
|
|
3111
|
+
var rows = tbody.querySelectorAll('tr');
|
|
3112
|
+
for (var i = 0; i < rows.length; i++) {
|
|
3113
|
+
var tr = rows[i];
|
|
3114
|
+
var raw = tr.getAttribute('data-m') || '';
|
|
3115
|
+
var name = '';
|
|
1872
3116
|
try {
|
|
1873
3117
|
name = decodeURIComponent(raw).toLowerCase();
|
|
1874
3118
|
} catch (e) {
|
|
@@ -1901,10 +3145,14 @@ function pickMetricByKeyword(items, includeKeywords, excludeKeywords) {
|
|
|
1901
3145
|
return '';
|
|
1902
3146
|
}
|
|
1903
3147
|
|
|
1904
|
-
async function fetchMetricSeriesSafe(metricName, minutes, stepSec) {
|
|
3148
|
+
async function fetchMetricSeriesSafe(metricName, minutes, stepSec, aggregate, timeFrom, timeTo) {
|
|
1905
3149
|
if (!metricName) return { metricName: '', metricType: 'untyped', points: [], series: [] };
|
|
1906
3150
|
try {
|
|
1907
|
-
|
|
3151
|
+
var agg = aggregate === false ? '0' : '1';
|
|
3152
|
+
var q = '/metrics/series?metric=' + encodeURIComponent(metricName) + '&minutes=' + minutes + '&stepSec=' + stepSec + '&aggregate=' + agg;
|
|
3153
|
+
if (timeFrom) q += '&timeFrom=' + encodeURIComponent(String(timeFrom));
|
|
3154
|
+
if (timeTo) q += '&timeTo=' + encodeURIComponent(String(timeTo));
|
|
3155
|
+
return await fetchApi(q);
|
|
1908
3156
|
} catch (_) {
|
|
1909
3157
|
return { metricName: metricName, metricType: 'untyped', points: [], series: [] };
|
|
1910
3158
|
}
|
|
@@ -1957,10 +3205,205 @@ function metricSeriesRenderable(points) {
|
|
|
1957
3205
|
return Array.isArray(points) && points.length > 0;
|
|
1958
3206
|
}
|
|
1959
3207
|
|
|
3208
|
+
function metricSeriesLabel(s, idx) {
|
|
3209
|
+
if (s && s.labels && typeof s.labels === 'object') {
|
|
3210
|
+
var keys = Object.keys(s.labels);
|
|
3211
|
+
if (keys.length > 0) {
|
|
3212
|
+
var k = keys[0];
|
|
3213
|
+
return String(s.labels[k] || k || ('series_' + (idx + 1)));
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
return 'series_' + (idx + 1);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function formatPanelTimeLabel(ts) {
|
|
3220
|
+
if (!ts) return '-';
|
|
3221
|
+
try {
|
|
3222
|
+
var d = new Date(Number(ts));
|
|
3223
|
+
return d.toLocaleTimeString();
|
|
3224
|
+
} catch (_) {
|
|
3225
|
+
return '-';
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
function inferSeriesDomain(points, fallbackDomain, stepSec) {
|
|
3230
|
+
var fallbackStart = fallbackDomain && Number.isFinite(fallbackDomain.startMs) ? Number(fallbackDomain.startMs) : (Date.now() - 60 * 60 * 1000);
|
|
3231
|
+
var fallbackEnd = fallbackDomain && Number.isFinite(fallbackDomain.endMs) ? Number(fallbackDomain.endMs) : Date.now();
|
|
3232
|
+
if (!Array.isArray(points) || points.length === 0) {
|
|
3233
|
+
return { startMs: fallbackStart, endMs: fallbackEnd };
|
|
3234
|
+
}
|
|
3235
|
+
var minTs = Infinity;
|
|
3236
|
+
var maxTs = -Infinity;
|
|
3237
|
+
for (var i = 0; i < points.length; i++) {
|
|
3238
|
+
var ts = Number(points[i] && points[i].timestampMs || 0);
|
|
3239
|
+
if (!Number.isFinite(ts)) continue;
|
|
3240
|
+
if (ts < minTs) minTs = ts;
|
|
3241
|
+
if (ts > maxTs) maxTs = ts;
|
|
3242
|
+
}
|
|
3243
|
+
if (!Number.isFinite(minTs) || !Number.isFinite(maxTs)) {
|
|
3244
|
+
return { startMs: fallbackStart, endMs: fallbackEnd };
|
|
3245
|
+
}
|
|
3246
|
+
var stepMs = Math.max(5, Number(stepSec || 30)) * 1000;
|
|
3247
|
+
var pad = Math.max(stepMs * 2, 30 * 1000);
|
|
3248
|
+
var start = minTs - pad;
|
|
3249
|
+
var end = maxTs + pad;
|
|
3250
|
+
if (end <= start) end = start + stepMs;
|
|
3251
|
+
return { startMs: start, endMs: end };
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
function collectAllPoints(series) {
|
|
3255
|
+
var out = [];
|
|
3256
|
+
if (!series || !Array.isArray(series)) return out;
|
|
3257
|
+
for (var i = 0; i < series.length; i++) {
|
|
3258
|
+
var pts = series[i] && series[i].points;
|
|
3259
|
+
if (!Array.isArray(pts)) continue;
|
|
3260
|
+
for (var j = 0; j < pts.length; j++) out.push(pts[j]);
|
|
3261
|
+
}
|
|
3262
|
+
return out;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
function buildMetricsPanel(metricName, detailed, timeDomain) {
|
|
3266
|
+
var palette = ['#5b8ff9', '#5ad8a6', '#f6bd16', '#e8684a', '#6dc8ec', '#9270ca', '#ff9d4d', '#269a99'];
|
|
3267
|
+
var series = (detailed && Array.isArray(detailed.series)) ? detailed.series.filter(function(s) {
|
|
3268
|
+
return s && Array.isArray(s.points) && s.points.length > 0;
|
|
3269
|
+
}) : [];
|
|
3270
|
+
if (!series.length && detailed && Array.isArray(detailed.points) && detailed.points.length > 0) {
|
|
3271
|
+
series = [{ seriesId: 'aggregate', labels: {}, points: detailed.points }];
|
|
3272
|
+
}
|
|
3273
|
+
if (!series.length) return '';
|
|
3274
|
+
|
|
3275
|
+
var width = 860;
|
|
3276
|
+
var height = 260;
|
|
3277
|
+
var padLeft = 52;
|
|
3278
|
+
var padRight = 16;
|
|
3279
|
+
var padTop = 12;
|
|
3280
|
+
var padBottom = 32;
|
|
3281
|
+
var allPoints = [];
|
|
3282
|
+
series.forEach(function(s) {
|
|
3283
|
+
(s.points || []).forEach(function(p) { allPoints.push(p); });
|
|
3284
|
+
});
|
|
3285
|
+
var minY = Math.min.apply(null, allPoints.map(function(p) { return Number(p.value || 0); }));
|
|
3286
|
+
var maxY = Math.max.apply(null, allPoints.map(function(p) { return Number(p.value || 0); }));
|
|
3287
|
+
if (!Number.isFinite(minY)) minY = 0;
|
|
3288
|
+
if (!Number.isFinite(maxY)) maxY = 1;
|
|
3289
|
+
minY = 0;
|
|
3290
|
+
if (minY === maxY) maxY = minY + 1;
|
|
3291
|
+
var plotW = width - padLeft - padRight;
|
|
3292
|
+
var plotH = height - padTop - padBottom;
|
|
3293
|
+
var startMs = timeDomain && timeDomain.startMs != null ? Number(timeDomain.startMs) : NaN;
|
|
3294
|
+
var endMs = timeDomain && timeDomain.endMs != null ? Number(timeDomain.endMs) : NaN;
|
|
3295
|
+
var useTimeX = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs;
|
|
3296
|
+
var yTickCount = 4;
|
|
3297
|
+
var xTickCount = 4;
|
|
3298
|
+
function xFor(tsRaw, index, len) {
|
|
3299
|
+
if (!useTimeX) return padLeft + (index / Math.max(len - 1, 1)) * plotW;
|
|
3300
|
+
var ts = Number(tsRaw || 0);
|
|
3301
|
+
if (ts < startMs) ts = startMs;
|
|
3302
|
+
if (ts > endMs) ts = endMs;
|
|
3303
|
+
return padLeft + ((ts - startMs) / (endMs - startMs)) * plotW;
|
|
3304
|
+
}
|
|
3305
|
+
function yFor(vRaw) {
|
|
3306
|
+
var v = Number(vRaw || 0);
|
|
3307
|
+
return padTop + (1 - ((v - minY) / (maxY - minY))) * plotH;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
var normalizedSeries = series.slice(0, 6).map(function(s, idx) {
|
|
3311
|
+
var name = metricSeriesLabel(s, idx);
|
|
3312
|
+
var color = palette[idx % palette.length];
|
|
3313
|
+
var points = (s.points || []).map(function(p, i) {
|
|
3314
|
+
var ts = Number(p.timestampMs || 0);
|
|
3315
|
+
var value = Number(p.value || 0);
|
|
3316
|
+
return { timestampMs: ts, value: value, x: xFor(ts, i, (s.points || []).length), y: yFor(value) };
|
|
3317
|
+
});
|
|
3318
|
+
var byTs = {};
|
|
3319
|
+
points.forEach(function(p) { byTs[String(p.timestampMs)] = p.value; });
|
|
3320
|
+
return { name: name, color: color, points: points, byTs: byTs };
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3323
|
+
var tsMap = {};
|
|
3324
|
+
normalizedSeries.forEach(function(s) {
|
|
3325
|
+
s.points.forEach(function(p) { tsMap[String(p.timestampMs)] = true; });
|
|
3326
|
+
});
|
|
3327
|
+
var timeline = Object.keys(tsMap).map(function(k) { return Number(k); }).filter(function(v) { return Number.isFinite(v); }).sort(function(a, b) { return a - b; });
|
|
3328
|
+
var rows = timeline.map(function(ts) {
|
|
3329
|
+
return {
|
|
3330
|
+
timestampMs: ts,
|
|
3331
|
+
x: xFor(ts, 0, 1),
|
|
3332
|
+
values: normalizedSeries.map(function(s) {
|
|
3333
|
+
var has = Object.prototype.hasOwnProperty.call(s.byTs, String(ts));
|
|
3334
|
+
return { name: s.name, color: s.color, value: has ? Number(s.byTs[String(ts)]) : null };
|
|
3335
|
+
})
|
|
3336
|
+
};
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
var panelId = 'panel-' + (++metricsPanelSeq);
|
|
3340
|
+
metricsPanelStore[panelId] = {
|
|
3341
|
+
width: width,
|
|
3342
|
+
height: height,
|
|
3343
|
+
padLeft: padLeft,
|
|
3344
|
+
padRight: padRight,
|
|
3345
|
+
padTop: padTop,
|
|
3346
|
+
padBottom: padBottom,
|
|
3347
|
+
plotW: plotW,
|
|
3348
|
+
plotH: plotH,
|
|
3349
|
+
rows: rows,
|
|
3350
|
+
minY: minY,
|
|
3351
|
+
maxY: maxY
|
|
3352
|
+
};
|
|
3353
|
+
|
|
3354
|
+
var h = '';
|
|
3355
|
+
h += '<div class="metrics-panel">';
|
|
3356
|
+
h += '<div class="metrics-panel-head">';
|
|
3357
|
+
h += '<div class="metrics-panel-title">' + esc(metricName) + '</div>';
|
|
3358
|
+
h += '<div class="metrics-panel-sub">' + normalizedSeries.length + ' series</div>';
|
|
3359
|
+
h += '</div>';
|
|
3360
|
+
h += '<div class="metrics-panel-chart" data-panel-id="' + panelId + '">';
|
|
3361
|
+
h += '<svg class="metrics-panel-svg" viewBox="0 0 ' + width + ' ' + height + '">';
|
|
3362
|
+
for (var yi = 0; yi <= yTickCount; yi++) {
|
|
3363
|
+
var ratioY = yi / yTickCount;
|
|
3364
|
+
var y = padTop + ratioY * plotH;
|
|
3365
|
+
var yVal = maxY - ratioY * (maxY - minY);
|
|
3366
|
+
h += '<line x1="' + padLeft + '" y1="' + y.toFixed(1) + '" x2="' + (width - padRight) + '" y2="' + y.toFixed(1) + '" stroke="var(--border)" stroke-width="1" opacity="' + (yi === yTickCount ? '1' : '.55') + '" />';
|
|
3367
|
+
h += '<text x="' + (padLeft - 8) + '" y="' + (y + 4).toFixed(1) + '" text-anchor="end" font-size="10" fill="var(--muted)">' + esc(fmtNum(yVal)) + '</text>';
|
|
3368
|
+
}
|
|
3369
|
+
for (var xi = 0; xi <= xTickCount; xi++) {
|
|
3370
|
+
var ratioX = xi / xTickCount;
|
|
3371
|
+
var x = padLeft + ratioX * plotW;
|
|
3372
|
+
var tickTs = useTimeX ? Math.round(startMs + ratioX * (endMs - startMs)) : 0;
|
|
3373
|
+
h += '<line x1="' + x.toFixed(1) + '" y1="' + padTop + '" x2="' + x.toFixed(1) + '" y2="' + (height - padBottom) + '" stroke="var(--border)" stroke-width="1" opacity=".35" />';
|
|
3374
|
+
h += '<text x="' + x.toFixed(1) + '" y="' + (height - 8) + '" text-anchor="middle" font-size="10" fill="var(--muted)">' + esc(useTimeX ? formatPanelTimeLabel(tickTs) : '') + '</text>';
|
|
3375
|
+
}
|
|
3376
|
+
h += '<line x1="' + padLeft + '" y1="' + (height - padBottom) + '" x2="' + (width - padRight) + '" y2="' + (height - padBottom) + '" stroke="var(--border)" stroke-width="1" />';
|
|
3377
|
+
h += '<line x1="' + padLeft + '" y1="' + padTop + '" x2="' + padLeft + '" y2="' + (height - padBottom) + '" stroke="var(--border)" stroke-width="1" />';
|
|
3378
|
+
|
|
3379
|
+
normalizedSeries.forEach(function(s) {
|
|
3380
|
+
var pts = (s.points || []).map(function(p) {
|
|
3381
|
+
return p.x.toFixed(1) + ',' + p.y.toFixed(1);
|
|
3382
|
+
}).join(' ');
|
|
3383
|
+
if (!pts) return;
|
|
3384
|
+
h += '<polyline fill="none" stroke="' + s.color + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="' + pts + '"></polyline>';
|
|
3385
|
+
});
|
|
3386
|
+
h += '</svg>';
|
|
3387
|
+
h += '<div class="metrics-panel-overlay" aria-hidden="true"></div>';
|
|
3388
|
+
h += '<div class="metrics-panel-marker"></div>';
|
|
3389
|
+
normalizedSeries.forEach(function(s, idx) {
|
|
3390
|
+
h += '<div class="metrics-panel-point" data-i="' + idx + '" style="background:' + s.color + ';"></div>';
|
|
3391
|
+
});
|
|
3392
|
+
h += '<div class="metrics-panel-tip"></div>';
|
|
3393
|
+
h += '</div>';
|
|
3394
|
+
h += '<div class="metrics-panel-legend">';
|
|
3395
|
+
normalizedSeries.forEach(function(s) {
|
|
3396
|
+
h += '<div class="metrics-panel-legend-item"><span class="metrics-panel-dot" style="background:' + s.color + '"></span>' + esc(s.name) + '</div>';
|
|
3397
|
+
});
|
|
3398
|
+
h += '</div>';
|
|
3399
|
+
h += '</div>';
|
|
3400
|
+
return h;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
1960
3403
|
function buildMetricCard(title, subtitle, metricName, series, unit, timeDomain) {
|
|
1961
3404
|
var points = (series && series.points) || [];
|
|
1962
3405
|
var latest = points.length ? Number(points[points.length - 1].value || 0) : 0;
|
|
1963
|
-
var metricLabel = metricName ? ('Metric: ' + esc(metricName)) : 'Metric: unavailable';
|
|
3406
|
+
var metricLabel = metricName ? (esc(tr('Metric', '指标')) + ': ' + esc(metricName)) : (esc(tr('Metric', '指标')) + ': ' + esc(tr('unavailable', '不可用')));
|
|
1964
3407
|
var valueLabel = points.length ? (fmtNum(latest) + (unit || '')) : '-';
|
|
1965
3408
|
var h = '<div class="an-card">';
|
|
1966
3409
|
h += '<h3><span class="icon">📊</span> ' + esc(title) + '<span class="sub">' + esc(valueLabel) + '</span></h3>';
|
|
@@ -1976,7 +3419,7 @@ function buildMetricCard(title, subtitle, metricName, series, unit, timeDomain)
|
|
|
1976
3419
|
|
|
1977
3420
|
function buildMetricSparkline(points, timeDomain) {
|
|
1978
3421
|
if (!points || !points.length) {
|
|
1979
|
-
return '<div class="empty" style="padding:16px"><div class="text">No points</div></div>';
|
|
3422
|
+
return '<div class="empty" style="padding:16px"><div class="text">' + esc(tr('No points', '暂无数据点')) + '</div></div>';
|
|
1980
3423
|
}
|
|
1981
3424
|
var width = 860;
|
|
1982
3425
|
var height = 220;
|
|
@@ -1986,6 +3429,7 @@ function buildMetricSparkline(points, timeDomain) {
|
|
|
1986
3429
|
var maxY = Math.max.apply(null, points.map(function(p) { return Number(p.value || 0); }));
|
|
1987
3430
|
if (!Number.isFinite(minY)) minY = 0;
|
|
1988
3431
|
if (!Number.isFinite(maxY)) maxY = 1;
|
|
3432
|
+
minY = 0;
|
|
1989
3433
|
if (minY === maxY) maxY = minY + 1;
|
|
1990
3434
|
var plotW = width - padX * 2;
|
|
1991
3435
|
var plotH = height - padY * 2;
|
|
@@ -2046,7 +3490,7 @@ function buildMetricSparkline(points, timeDomain) {
|
|
|
2046
3490
|
html += '<div class="spark-marker"></div>';
|
|
2047
3491
|
html += '<div class="spark-dot"></div>';
|
|
2048
3492
|
html += '<div class="spark-tip"></div>';
|
|
2049
|
-
html += '<div class="kpi-sub">Latest point at ' + esc(latestLabel) + ' • Range: ' + fmtNum(minY) + ' ~ ' + fmtNum(maxY) + '</div>';
|
|
3493
|
+
html += '<div class="kpi-sub">' + esc(tr('Latest point at', '最新点时间')) + ' ' + esc(latestLabel) + ' • ' + esc(tr('Range', '范围')) + ': ' + fmtNum(minY) + ' ~ ' + fmtNum(maxY) + '</div>';
|
|
2050
3494
|
html += '</div>';
|
|
2051
3495
|
return html;
|
|
2052
3496
|
}
|
|
@@ -2112,7 +3556,7 @@ function sparklineMove(ev, overlay) {
|
|
|
2112
3556
|
dot.style.top = (10 + (point.y / data.height) * rect.height) + 'px';
|
|
2113
3557
|
|
|
2114
3558
|
var t = point.timestampMs ? new Date(point.timestampMs).toLocaleString() : '-';
|
|
2115
|
-
tip.innerHTML = '<div><b>
|
|
3559
|
+
tip.innerHTML = '<div><b>' + esc(t('time')) + ':</b> ' + esc(t) + '</div><div><b>' + esc(tr('Value', '值')) + ':</b> ' + fmtNum(point.value) + '</div>';
|
|
2116
3560
|
tip.style.display = 'block';
|
|
2117
3561
|
var tipLeft = 10 + (point.x / data.width) * rect.width - 120;
|
|
2118
3562
|
tipLeft = Math.max(10, Math.min(rect.width - 250, tipLeft));
|
|
@@ -2131,11 +3575,114 @@ function sparklineLeave(overlay) {
|
|
|
2131
3575
|
if (tip) tip.style.display = 'none';
|
|
2132
3576
|
}
|
|
2133
3577
|
|
|
3578
|
+
function metricsPanelMove(ev, overlay) {
|
|
3579
|
+
if (!overlay) return;
|
|
3580
|
+
var chart = overlay.closest('.metrics-panel-chart');
|
|
3581
|
+
if (!chart) return;
|
|
3582
|
+
var panelId = chart.getAttribute('data-panel-id') || '';
|
|
3583
|
+
var data = metricsPanelStore[panelId];
|
|
3584
|
+
if (!data || !data.rows || !data.rows.length) return;
|
|
3585
|
+
|
|
3586
|
+
var rect = overlay.getBoundingClientRect();
|
|
3587
|
+
if (rect.width < 2 || rect.height < 2) return;
|
|
3588
|
+
|
|
3589
|
+
var clientX = 0;
|
|
3590
|
+
var clientY = 0;
|
|
3591
|
+
if (ev && ev.touches && ev.touches.length) {
|
|
3592
|
+
clientX = ev.touches[0].clientX;
|
|
3593
|
+
clientY = ev.touches[0].clientY;
|
|
3594
|
+
} else if (ev && ev.clientX != null) {
|
|
3595
|
+
clientX = ev.clientX;
|
|
3596
|
+
clientY = ev.clientY != null ? ev.clientY : 0;
|
|
3597
|
+
} else {
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
var edge = 2;
|
|
3601
|
+
if (clientX < rect.left - edge || clientX > rect.right + edge || clientY < rect.top - edge || clientY > rect.bottom + edge) {
|
|
3602
|
+
metricsPanelLeave(overlay);
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
var localX = clientX - rect.left;
|
|
3607
|
+
var chartX = Math.max(data.padLeft, Math.min(data.width - data.padRight, localX * (data.width / Math.max(rect.width, 1))));
|
|
3608
|
+
|
|
3609
|
+
var row = null;
|
|
3610
|
+
var bestD = Infinity;
|
|
3611
|
+
for (var i = 0; i < data.rows.length; i++) {
|
|
3612
|
+
var d = Math.abs(data.rows[i].x - chartX);
|
|
3613
|
+
if (d < bestD) {
|
|
3614
|
+
bestD = d;
|
|
3615
|
+
row = data.rows[i];
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
if (!row) return;
|
|
3619
|
+
|
|
3620
|
+
var marker = chart.querySelector('.metrics-panel-marker');
|
|
3621
|
+
var tip = chart.querySelector('.metrics-panel-tip');
|
|
3622
|
+
if (!marker || !tip) return;
|
|
3623
|
+
|
|
3624
|
+
var markerX = 8 + (row.x / data.width) * rect.width;
|
|
3625
|
+
marker.style.display = 'block';
|
|
3626
|
+
marker.style.left = markerX + 'px';
|
|
3627
|
+
|
|
3628
|
+
var lines = ['<div><b>Time:</b> ' + esc(formatPanelTimeLabel(row.timestampMs)) + '</div>'];
|
|
3629
|
+
var points = chart.querySelectorAll('.metrics-panel-point');
|
|
3630
|
+
for (var j = 0; j < points.length; j++) {
|
|
3631
|
+
var node = points[j];
|
|
3632
|
+
var v = row.values[j];
|
|
3633
|
+
if (!v || v.value == null) {
|
|
3634
|
+
node.style.display = 'none';
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
var yRatio = (v.value - data.minY) / Math.max((data.maxY - data.minY), 1e-9);
|
|
3638
|
+
var y = data.padTop + (1 - yRatio) * data.plotH;
|
|
3639
|
+
node.style.display = 'block';
|
|
3640
|
+
node.style.left = (8 + (row.x / data.width) * rect.width) + 'px';
|
|
3641
|
+
node.style.top = (8 + (y / data.height) * rect.height) + 'px';
|
|
3642
|
+
lines.push('<div><span style="display:inline-block;width:8px;height:8px;border-radius:999px;background:' + esc(v.color) + ';margin-right:6px"></span>' + esc(v.name) + ': <b>' + esc(fmtNum(v.value)) + '</b></div>');
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
tip.innerHTML = lines.join('');
|
|
3646
|
+
tip.style.display = 'block';
|
|
3647
|
+
var tipLeft = markerX + 10;
|
|
3648
|
+
var maxLeft = Math.max(8, rect.width - 300);
|
|
3649
|
+
if (tipLeft > maxLeft) tipLeft = markerX - 270;
|
|
3650
|
+
tip.style.left = Math.max(8, tipLeft) + 'px';
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
function metricsPanelLeave(overlay) {
|
|
3654
|
+
if (!overlay) return;
|
|
3655
|
+
var chart = overlay.closest('.metrics-panel-chart');
|
|
3656
|
+
if (!chart) return;
|
|
3657
|
+
var marker = chart.querySelector('.metrics-panel-marker');
|
|
3658
|
+
var tip = chart.querySelector('.metrics-panel-tip');
|
|
3659
|
+
if (marker) marker.style.display = 'none';
|
|
3660
|
+
if (tip) tip.style.display = 'none';
|
|
3661
|
+
var points = chart.querySelectorAll('.metrics-panel-point');
|
|
3662
|
+
for (var i = 0; i < points.length; i++) {
|
|
3663
|
+
points[i].style.display = 'none';
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
|
|
2134
3667
|
async function renderMetrics() {
|
|
2135
|
-
app.innerHTML = renderLayout('metrics', '<div class="loading">
|
|
3668
|
+
app.innerHTML = renderLayout('metrics', '<div class="loading">' + esc(t('loading_metrics')) + '</div>');
|
|
2136
3669
|
|
|
2137
3670
|
try {
|
|
2138
|
-
|
|
3671
|
+
ensureMetricsTimeRangeDefaults();
|
|
3672
|
+
metricsPanelSeq = 0;
|
|
3673
|
+
metricsPanelStore = {};
|
|
3674
|
+
var mtf = getMetricsTimeFilter();
|
|
3675
|
+
var startMsForStep = mtf.timeFrom ? Date.parse(mtf.timeFrom) : (Date.now() - metricsRangeMinutes * 60 * 1000);
|
|
3676
|
+
var endMsForStep = mtf.timeTo ? Date.parse(mtf.timeTo) : Date.now();
|
|
3677
|
+
if (!Number.isFinite(startMsForStep)) startMsForStep = Date.now() - metricsRangeMinutes * 60 * 1000;
|
|
3678
|
+
if (!Number.isFinite(endMsForStep)) endMsForStep = Date.now();
|
|
3679
|
+
if (endMsForStep <= startMsForStep) endMsForStep = startMsForStep + 60 * 1000;
|
|
3680
|
+
var effectiveStepSec = computeAutoStepSec(startMsForStep, endMsForStep);
|
|
3681
|
+
metricsStepSec = effectiveStepSec;
|
|
3682
|
+
var overviewQs = '/metrics/overview?minutes=' + metricsRangeMinutes + '&limit=200';
|
|
3683
|
+
if (mtf.timeFrom) overviewQs += '&timeFrom=' + encodeURIComponent(mtf.timeFrom);
|
|
3684
|
+
if (mtf.timeTo) overviewQs += '&timeTo=' + encodeURIComponent(mtf.timeTo);
|
|
3685
|
+
var overview = await fetchApi(overviewQs);
|
|
2139
3686
|
|
|
2140
3687
|
var rawItems = overview && overview.items != null ? overview.items : [];
|
|
2141
3688
|
var items = Array.isArray(rawItems) ? rawItems.slice() : [];
|
|
@@ -2147,7 +3694,7 @@ async function renderMetrics() {
|
|
|
2147
3694
|
if (overview && overview.enabled === false && !hasCatalogSignal) {
|
|
2148
3695
|
var otlp = overview.otlpMetricsEndpoint || (overview.otlpPath ? overview.otlpPath + '/v1/metrics' : '-');
|
|
2149
3696
|
var disabledHtml = '';
|
|
2150
|
-
disabledHtml += '<div class="section-title">
|
|
3697
|
+
disabledHtml += '<div class="section-title">' + esc(t('nav_metrics')) + ' <span class="count">' + esc(t('metrics_disabled')) + '</span></div>';
|
|
2151
3698
|
disabledHtml += '<div class="metrics-callout">';
|
|
2152
3699
|
disabledHtml += '<div class="metrics-callout-title">OTLP metrics ingest is off</div>';
|
|
2153
3700
|
disabledHtml += '<div class="metrics-callout-body">Enable metrics in the plugin config and point your OpenTelemetry exporter at the OTLP HTTP endpoint below.</div>';
|
|
@@ -2193,14 +3740,14 @@ async function renderMetrics() {
|
|
|
2193
3740
|
for (var fi = 0; fi < metricNeedFetch.length; fi += FETCH_CHUNK) {
|
|
2194
3741
|
var nameChunk = metricNeedFetch.slice(fi, fi + FETCH_CHUNK);
|
|
2195
3742
|
await Promise.all(nameChunk.map(async function(name) {
|
|
2196
|
-
metricSeriesMap[name] = await fetchMetricSeriesSafe(name, metricsRangeMinutes,
|
|
3743
|
+
metricSeriesMap[name] = await fetchMetricSeriesSafe(name, metricsRangeMinutes, effectiveStepSec, true, mtf.timeFrom, mtf.timeTo);
|
|
2197
3744
|
}));
|
|
2198
3745
|
}
|
|
2199
3746
|
|
|
2200
|
-
var msgProcessedRate = deriveCounterRateSeries((metricSeriesMap[important.messageProcessed] || { points: [] }).points,
|
|
2201
|
-
var tokenRate = deriveCounterRateSeries((metricSeriesMap[important.tokenTotal] || { points: [] }).points,
|
|
2202
|
-
var outputTokenRate = deriveCounterRateSeries((metricSeriesMap[important.tokenOutput] || { points: [] }).points,
|
|
2203
|
-
var costRate = deriveCounterRateSeries((metricSeriesMap[important.costUsd] || { points: [] }).points,
|
|
3747
|
+
var msgProcessedRate = deriveCounterRateSeries((metricSeriesMap[important.messageProcessed] || { points: [] }).points, effectiveStepSec, 60);
|
|
3748
|
+
var tokenRate = deriveCounterRateSeries((metricSeriesMap[important.tokenTotal] || { points: [] }).points, effectiveStepSec, 60);
|
|
3749
|
+
var outputTokenRate = deriveCounterRateSeries((metricSeriesMap[important.tokenOutput] || { points: [] }).points, effectiveStepSec, 60);
|
|
3750
|
+
var costRate = deriveCounterRateSeries((metricSeriesMap[important.costUsd] || { points: [] }).points, effectiveStepSec, 60);
|
|
2204
3751
|
var latencyAvg = deriveMeanFromCounterSeries(
|
|
2205
3752
|
(metricSeriesMap[important.durationSum] || { points: [] }).points,
|
|
2206
3753
|
(metricSeriesMap[important.durationCount] || { points: [] }).points
|
|
@@ -2220,110 +3767,545 @@ async function renderMetrics() {
|
|
|
2220
3767
|
itemsDisplay.sort(function(a, b) { return String(a.metricName || '').localeCompare(String(b.metricName || '')); });
|
|
2221
3768
|
}
|
|
2222
3769
|
|
|
2223
|
-
var html = '';
|
|
2224
|
-
html += '<div class="section-title">
|
|
3770
|
+
var html = '';
|
|
3771
|
+
html += '<div class="section-title">' + esc(t('nav_metrics')) + ' <span class="count">' + fmtNum(overview.totalMetrics || 0) + ' ' + esc(tr('metrics', '个指标')) + '</span></div>';
|
|
3772
|
+
|
|
3773
|
+
if (!items.length && !(overview.totalSamples > 0)) {
|
|
3774
|
+
html += '<div class="metrics-hint">' + esc(tr('No samples in the selected window. After OTLP metrics are ingested, use Refresh or widen the time range.', '当前时间范围无样本。待 OTLP 指标上报后,可点击刷新或扩大时间范围。')) + '</div>';
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
if (overview.lastError) {
|
|
3778
|
+
html += '<div class="empty" style="margin-bottom:12px"><div class="text">' + esc(tr('Ingest warning', '写入告警')) + ': ' + esc(String(overview.lastError)) + '</div></div>';
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
html += '<div class="metrics-toolbar">';
|
|
3782
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">Time window</span>';
|
|
3783
|
+
html += '<div class="time-dropdown">';
|
|
3784
|
+
html += '<button class="time-btn" onclick="metricsToggleRangeMenu(event)" aria-label="Metrics time window">';
|
|
3785
|
+
html += esc(getMetricsWindowLabel());
|
|
3786
|
+
html += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
3787
|
+
html += '</button>';
|
|
3788
|
+
html += '<div class="time-menu" id="metrics-time-menu">';
|
|
3789
|
+
METRICS_WINDOW_OPTIONS.forEach(function(m) {
|
|
3790
|
+
var cls = (metricsRangeMinutes === m) ? ' active' : '';
|
|
3791
|
+
html += '<div class="time-menu-item' + cls + '" onclick="metricsSelectRange(\\'' + m + '\\')"><span class="check">' + (metricsRangeMinutes===m ? '✓' : '') + '</span>' + m + ' min</div>';
|
|
3792
|
+
});
|
|
3793
|
+
html += '</div></div></div>';
|
|
3794
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">' + esc(t('start')) + '</span><input class="time-input" type="datetime-local" id="m-time-from" value="' + esc(toLocalDateTimeInputValue(metricsTimeFrom)) + '"></div>';
|
|
3795
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">' + esc(t('end')) + '</span><input class="time-input" type="datetime-local" id="m-time-to" value="' + esc(toLocalDateTimeInputValue(metricsTimeTo)) + '"></div>';
|
|
3796
|
+
html += '<button type="button" class="btn-apply" onclick="metricsApplyTimeInputs()" title="' + esc(t('apply_time_range')) + '">' + ICON_CHECK + '</button>';
|
|
3797
|
+
html += '<button type="button" class="icon-refresh-btn" onclick="renderMetrics()" title="' + esc(t('refresh_metrics')) + '">' + ICON_REFRESH + '</button>';
|
|
3798
|
+
html += '</div>';
|
|
3799
|
+
|
|
3800
|
+
var chartNowMs = Date.now();
|
|
3801
|
+
var startMs = mtf.timeFrom ? Date.parse(mtf.timeFrom) : (chartNowMs - metricsRangeMinutes * 60 * 1000);
|
|
3802
|
+
var endMs = mtf.timeTo ? Date.parse(mtf.timeTo) : chartNowMs;
|
|
3803
|
+
if (!Number.isFinite(startMs)) startMs = chartNowMs - metricsRangeMinutes * 60 * 1000;
|
|
3804
|
+
if (!Number.isFinite(endMs)) endMs = chartNowMs;
|
|
3805
|
+
if (endMs <= startMs) endMs = startMs + Math.max(effectiveStepSec, 30) * 1000;
|
|
3806
|
+
var metricsChartDomain = { startMs: startMs, endMs: endMs };
|
|
3807
|
+
var featuredNames = itemsDisplay.slice(0, 4).map(function(m) { return String(m.metricName || ''); }).filter(Boolean);
|
|
3808
|
+
var featuredDetailMap = {};
|
|
3809
|
+
if (featuredNames.length > 0) {
|
|
3810
|
+
await Promise.all(featuredNames.map(async function(name) {
|
|
3811
|
+
featuredDetailMap[name] = await fetchMetricSeriesSafe(name, metricsRangeMinutes, effectiveStepSec, false, mtf.timeFrom, mtf.timeTo);
|
|
3812
|
+
}));
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
var panelBlocks = featuredNames.map(function(name) {
|
|
3816
|
+
var detail = featuredDetailMap[name];
|
|
3817
|
+
return buildMetricsPanel(name, detail, metricsChartDomain);
|
|
3818
|
+
}).filter(function(s) { return !!s; });
|
|
3819
|
+
if (panelBlocks.length > 0) {
|
|
3820
|
+
html += '<div class="metrics-panel-grid">';
|
|
3821
|
+
panelBlocks.forEach(function(block) { html += block; });
|
|
3822
|
+
html += '</div>';
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
/* KPI charts: importance order; omit cards with no drawable series */
|
|
3826
|
+
var kpiCards = [];
|
|
3827
|
+
if (important.tokenTotal && metricSeriesRenderable(tokenRate)) {
|
|
3828
|
+
kpiCards.push(buildMetricCard('Tokens / min', 'Total token throughput', important.tokenTotal, { points: tokenRate }, ' /min', metricsChartDomain));
|
|
3829
|
+
}
|
|
3830
|
+
if (important.tokenOutput && metricSeriesRenderable(outputTokenRate)) {
|
|
3831
|
+
kpiCards.push(buildMetricCard('Output Tokens / min', 'Assistant output rate', important.tokenOutput, { points: outputTokenRate }, ' /min', metricsChartDomain));
|
|
3832
|
+
}
|
|
3833
|
+
if (important.messageProcessed && metricSeriesRenderable(msgProcessedRate)) {
|
|
3834
|
+
kpiCards.push(buildMetricCard('Messages / min', 'Processed throughput', important.messageProcessed, { points: msgProcessedRate }, ' /min', metricsChartDomain));
|
|
3835
|
+
}
|
|
3836
|
+
if (important.costUsd && metricSeriesRenderable(costRate)) {
|
|
3837
|
+
kpiCards.push(buildMetricCard('Cost / min', 'USD burn rate', important.costUsd, { points: costRate }, ' USD/min', metricsChartDomain));
|
|
3838
|
+
}
|
|
3839
|
+
if (important.durationSum && important.durationCount && metricSeriesRenderable(latencyAvg)) {
|
|
3840
|
+
kpiCards.push(buildMetricCard('Avg Message Duration', 'From histogram sum ÷ count', important.durationSum + ' + ' + important.durationCount, { points: latencyAvg }, ' ms', metricsChartDomain));
|
|
3841
|
+
}
|
|
3842
|
+
if (important.queueDepth && metricSeriesRenderable(queueDepthSeries)) {
|
|
3843
|
+
kpiCards.push(buildMetricCard('Queue Depth', 'Current queue pressure', important.queueDepth, { points: queueDepthSeries }, '', metricsChartDomain));
|
|
3844
|
+
}
|
|
3845
|
+
if (important.contextSum && important.contextCount && metricSeriesRenderable(contextAvg)) {
|
|
3846
|
+
kpiCards.push(buildMetricCard('Context Tokens / msg', 'Average context usage', important.contextSum + ' + ' + important.contextCount, { points: contextAvg }, '', metricsChartDomain));
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
html += '<div class="metrics-kpi-grid">';
|
|
3850
|
+
if (!kpiCards.length) {
|
|
3851
|
+
html += '<div class="an-card"><div class="kpi-sub">' + esc(tr('No KPI charts for the current catalog (missing counters or not enough buckets in this window). See per-metric series and the table below.', '当前指标目录暂无可绘制 KPI(可能缺少 counter 或窗口桶数量不足)。请查看下方按指标时间序列与表格。')) + '</div></div>';
|
|
3852
|
+
} else {
|
|
3853
|
+
kpiCards.forEach(function(block) { html += block; });
|
|
3854
|
+
}
|
|
3855
|
+
html += '</div>';
|
|
3856
|
+
|
|
3857
|
+
html += '<div class="section-title" style="margin-top:8px">' + esc(tr('Time series', '时间序列')) + ' <span class="count">' + fmtNum(itemsDisplay.length) + ' ' + esc(tr('metrics', '个指标')) + '</span></div>';
|
|
3858
|
+
html += '<div class="metrics-series-grid">';
|
|
3859
|
+
itemsDisplay.forEach(function(m) {
|
|
3860
|
+
var nm = m.metricName;
|
|
3861
|
+
var enc = encodeURIComponent(nm);
|
|
3862
|
+
var s = metricSeriesMap[nm] || { points: [], metricType: m.metricType || 'untyped' };
|
|
3863
|
+
html += '<div class="an-card" data-spark-metric="' + enc + '">';
|
|
3864
|
+
html += '<h3><span class="icon">📈</span> ' + esc(nm) + '<span class="sub">' + esc(s.metricType || m.metricType || 'untyped') + ' · ' + effectiveStepSec + 's buckets</span></h3>';
|
|
3865
|
+
html += buildMetricSparkline((s && s.points) || [], metricsChartDomain);
|
|
3866
|
+
html += '</div>';
|
|
3867
|
+
});
|
|
3868
|
+
html += '</div>';
|
|
3869
|
+
|
|
3870
|
+
html += '<div class="an-card">';
|
|
3871
|
+
html += '<h3><span class="icon">🧾</span> ' + esc(t('latest_metrics_snapshot')) + ' <span class="sub">' + esc(tr('sum across labels · click a row to jump to its chart', '按 label 聚合求和 · 点击行可跳转到对应图表')) + '</span></h3>';
|
|
3872
|
+
html += '<div class="metrics-snapshot-tools">';
|
|
3873
|
+
html += '<input type="search" placeholder="' + esc(t('filter_by_metric_name')) + '" aria-label="' + esc(t('filter_by_metric_name')) + '" value="' + esc(metricsSnapshotFilter) + '" oninput="metricsTableFilterInput(this.value)" />';
|
|
3874
|
+
html += '</div>';
|
|
3875
|
+
html += '<div class="an-table-scroll"><table class="an-table" id="metrics-snapshot-table">';
|
|
3876
|
+
html += '<thead><tr><th>Metric</th><th>Type</th><th>Latest Value</th><th>Latest Timestamp</th><th>Samples</th></tr></thead>';
|
|
3877
|
+
html += '<tbody id="metrics-snapshot-tbody">';
|
|
3878
|
+
itemsDisplay.forEach(function(m) {
|
|
3879
|
+
var enc = encodeURIComponent(m.metricName);
|
|
3880
|
+
html += '<tr class="metrics-row' + (m.metricName===metricsSelected?' metrics-row-active':'') + '" data-m="' + enc + '" role="button" tabindex="0" aria-label="Jump to chart for ' + esc(m.metricName) + '" title="Jump to chart (click or Enter)">';
|
|
3881
|
+
html += '<td class="mono">' + esc(m.metricName) + '</td>';
|
|
3882
|
+
html += '<td>' + esc(m.metricType || 'untyped') + '</td>';
|
|
3883
|
+
html += '<td class="mono">' + fmtNum(Number(m.latestValue || 0)) + '</td>';
|
|
3884
|
+
html += '<td class="mono">' + (m.latestTimestampMs ? fmtTime(m.latestTimestampMs) : '-') + '</td>';
|
|
3885
|
+
html += '<td class="mono">' + fmtNum(Number(m.samples || 0)) + '</td>';
|
|
3886
|
+
html += '</tr>';
|
|
3887
|
+
});
|
|
3888
|
+
html += '</tbody></table></div>';
|
|
3889
|
+
html += '</div>';
|
|
3890
|
+
|
|
3891
|
+
app.innerHTML = renderLayout('metrics', html);
|
|
3892
|
+
requestAnimationFrame(function() { metricsTableFilterInput(metricsSnapshotFilter); });
|
|
3893
|
+
} catch(err) {
|
|
3894
|
+
app.innerHTML = renderLayout('metrics',
|
|
3895
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>');
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
async function ensureReplayProviders() {
|
|
3900
|
+
if (Array.isArray(replayProvidersCache) && replayProvidersCache.length > 0) return replayProvidersCache;
|
|
3901
|
+
var data = await fetchApi('/llm/providers');
|
|
3902
|
+
var providers = data && Array.isArray(data.providers) ? data.providers : [];
|
|
3903
|
+
replayProvidersCache = providers.slice();
|
|
3904
|
+
if (!replayState.providerId && providers.length > 0) {
|
|
3905
|
+
replayState.providerId = String(providers[0].providerId || '');
|
|
3906
|
+
}
|
|
3907
|
+
if (!replayState.model) {
|
|
3908
|
+
var p0 = providers[0];
|
|
3909
|
+
if (p0 && Array.isArray(p0.models) && p0.models.length > 0) {
|
|
3910
|
+
replayState.model = String(p0.models[0].id || '');
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return replayProvidersCache;
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
function getReplayProviderModels(providerId) {
|
|
3917
|
+
var providers = Array.isArray(replayProvidersCache) ? replayProvidersCache : [];
|
|
3918
|
+
var provider = providers.find(function(p) { return String(p.providerId || '') === String(providerId || ''); });
|
|
3919
|
+
if (!provider || !Array.isArray(provider.models)) return [];
|
|
3920
|
+
return provider.models.slice();
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
function replayExtractText(output) {
|
|
3924
|
+
if (!output || typeof output !== 'object') return '';
|
|
3925
|
+
if (typeof output.text === 'string' && output.text.trim()) return output.text.trim();
|
|
3926
|
+
if (Array.isArray(output.assistantTexts)) {
|
|
3927
|
+
var list = output.assistantTexts.filter(function(x){ return typeof x === 'string' && x.trim(); });
|
|
3928
|
+
if (list.length) return list.join('\\n\\n');
|
|
3929
|
+
}
|
|
3930
|
+
if (Array.isArray(output.content)) {
|
|
3931
|
+
var c = output.content
|
|
3932
|
+
.map(function(p){
|
|
3933
|
+
if (typeof p === 'string') return p;
|
|
3934
|
+
if (!p || typeof p !== 'object') return '';
|
|
3935
|
+
if (typeof p.text === 'string') return p.text;
|
|
3936
|
+
return '';
|
|
3937
|
+
})
|
|
3938
|
+
.filter(Boolean);
|
|
3939
|
+
if (c.length) return c.join('\\n\\n');
|
|
3940
|
+
}
|
|
3941
|
+
return '';
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
function replayExtractTextFromContent(content) {
|
|
3945
|
+
if (typeof content === 'string') return content;
|
|
3946
|
+
if (!Array.isArray(content)) return '';
|
|
3947
|
+
var out = [];
|
|
3948
|
+
for (var i = 0; i < content.length; i++) {
|
|
3949
|
+
var part = content[i];
|
|
3950
|
+
if (typeof part === 'string') {
|
|
3951
|
+
if (part.trim()) out.push(part.trim());
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3954
|
+
if (!part || typeof part !== 'object') continue;
|
|
3955
|
+
var t = String(part.type || '');
|
|
3956
|
+
var txt = typeof part.text === 'string' ? part.text : '';
|
|
3957
|
+
if ((t === 'text' || t === 'input_text' || t === 'output_text') && txt.trim()) {
|
|
3958
|
+
out.push(txt.trim());
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return out.join('\\n\\n').trim();
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
function replayNormalizePrompt(raw) {
|
|
3965
|
+
var s = String(raw || '');
|
|
3966
|
+
if (!s) return '';
|
|
3967
|
+
s = s.replace(/^\\s*Sender \\(untrusted metadata\\):\\s*(?:\\x60\\x60\\x60json[\\s\\S]*?\\x60\\x60\\x60)\\s*/i, '');
|
|
3968
|
+
s = s.replace(/^\\s*\\[[^\\]]+\\]\\s*/i, '');
|
|
3969
|
+
s = s.trim();
|
|
3970
|
+
if (/^A new session was started via \\/new or \\/reset\\./i.test(s)) return '';
|
|
3971
|
+
return s;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
function replayExtractUserPromptFromInput(input) {
|
|
3975
|
+
if (!input || typeof input !== 'object') return '';
|
|
3976
|
+
var direct = '';
|
|
3977
|
+
if (typeof input.userMessage === 'string') direct = input.userMessage;
|
|
3978
|
+
else if (typeof input.prompt === 'string') direct = input.prompt;
|
|
3979
|
+
direct = replayNormalizePrompt(direct);
|
|
3980
|
+
if (direct) return direct;
|
|
3981
|
+
|
|
3982
|
+
var hist = input.historyMessages;
|
|
3983
|
+
if (Array.isArray(hist)) {
|
|
3984
|
+
for (var i = hist.length - 1; i >= 0; i--) {
|
|
3985
|
+
var msg = hist[i];
|
|
3986
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
3987
|
+
var role = String(msg.role || '').toLowerCase();
|
|
3988
|
+
if (role !== 'user') continue;
|
|
3989
|
+
var txt = replayExtractTextFromContent(msg.content);
|
|
3990
|
+
txt = replayNormalizePrompt(txt);
|
|
3991
|
+
if (txt) return txt;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
return '';
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
function isReplayableAction(a) {
|
|
3998
|
+
if (!a || typeof a !== 'object') return false;
|
|
3999
|
+
var actionType = String(a.action_type || '');
|
|
4000
|
+
var actionName = String(a.action_name || '');
|
|
4001
|
+
return (
|
|
4002
|
+
(actionType === 'message' && actionName.indexOf('llm_call:') === 0)
|
|
4003
|
+
|| (actionType === 'replay' && actionName.indexOf('replay_call:') === 0)
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
window.openReplayFromAction = function(fi) {
|
|
4008
|
+
var spans = window.__traceSpans || [];
|
|
4009
|
+
if (fi == null || fi < 0 || fi >= spans.length) return;
|
|
4010
|
+
var span = spans[fi];
|
|
4011
|
+
if (!span || !span.action) return;
|
|
4012
|
+
var a = span.action;
|
|
4013
|
+
if (!isReplayableAction(a)) return;
|
|
4014
|
+
var input = parseJson(a.input_params);
|
|
4015
|
+
var output = parseJson(a.output_result);
|
|
4016
|
+
var modelName = String(a.model_name || '');
|
|
4017
|
+
var providerId = '';
|
|
4018
|
+
var model = modelName;
|
|
4019
|
+
if (modelName.indexOf('/') > 0) {
|
|
4020
|
+
providerId = modelName.split('/')[0];
|
|
4021
|
+
model = modelName.slice(providerId.length + 1);
|
|
4022
|
+
}
|
|
4023
|
+
replayState.providerId = providerId || replayState.providerId || '';
|
|
4024
|
+
replayState.model = String(model || replayState.model || '').trim();
|
|
4025
|
+
replayState.systemPrompt = input && typeof input.systemPrompt === 'string' ? input.systemPrompt : '';
|
|
4026
|
+
replayState.userPrompt = replayExtractUserPromptFromInput(input);
|
|
4027
|
+
replayState.replayInput = (input && typeof input === 'object') ? input : null;
|
|
4028
|
+
replayState.replayInputText = replayState.replayInput ? JSON.stringify(replayState.replayInput, null, 2) : '';
|
|
4029
|
+
replayState.replayInputError = '';
|
|
4030
|
+
replayState.error = '';
|
|
4031
|
+
replayState.result = null;
|
|
4032
|
+
replayState.sourceSessionId = String(a.session_id || currentTraceSessionId || '');
|
|
4033
|
+
replayState.sourceObservationId = String(a.__observation_id || a.id || '');
|
|
4034
|
+
replayState.sourceActionName = String(a.action_name || '');
|
|
4035
|
+
replayState.sourceCreatedAt = String(a.created_at || '');
|
|
4036
|
+
replayState.baselineText = replayExtractText(output);
|
|
4037
|
+
replayState.baselinePromptTokens = a.prompt_tokens != null ? Number(a.prompt_tokens) : (output && output.usage ? Number(output.usage.promptTokens || 0) : null);
|
|
4038
|
+
replayState.baselineCompletionTokens = a.completion_tokens != null ? Number(a.completion_tokens) : (output && output.usage ? Number(output.usage.completionTokens || 0) : null);
|
|
4039
|
+
replayState.baselineLatencyMs = a.duration_ms != null ? Number(a.duration_ms) : null;
|
|
4040
|
+
location.hash = '#/replay';
|
|
4041
|
+
};
|
|
4042
|
+
|
|
4043
|
+
window.replayChangeProvider = function(value) {
|
|
4044
|
+
replayState.providerId = String(value || '');
|
|
4045
|
+
var models = getReplayProviderModels(replayState.providerId);
|
|
4046
|
+
if (!models.some(function(m) { return String(m.id || '') === String(replayState.model || ''); })) {
|
|
4047
|
+
replayState.model = models.length > 0 ? String(models[0].id || '') : '';
|
|
4048
|
+
}
|
|
4049
|
+
replayState.error = '';
|
|
4050
|
+
replayState.result = null;
|
|
4051
|
+
renderReplay();
|
|
4052
|
+
};
|
|
4053
|
+
|
|
4054
|
+
window.replayChangeModel = function(value) {
|
|
4055
|
+
replayState.model = String(value || '').trim();
|
|
4056
|
+
};
|
|
4057
|
+
|
|
4058
|
+
function getReplayInputObject() {
|
|
4059
|
+
if (replayState.replayInput && typeof replayState.replayInput === 'object' && !Array.isArray(replayState.replayInput)) {
|
|
4060
|
+
return replayState.replayInput;
|
|
4061
|
+
}
|
|
4062
|
+
var raw = String(replayState.replayInputText || '').trim();
|
|
4063
|
+
if (!raw) return {};
|
|
4064
|
+
try {
|
|
4065
|
+
var parsed = JSON.parse(raw);
|
|
4066
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
4067
|
+
} catch {}
|
|
4068
|
+
return {};
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function syncReplayInputObject(nextObj) {
|
|
4072
|
+
replayState.replayInput = nextObj;
|
|
4073
|
+
replayState.replayInputText = JSON.stringify(nextObj, null, 2);
|
|
4074
|
+
replayState.replayInputError = '';
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
window.replayEditTopLevelValue = function(index, kind, value) {
|
|
4078
|
+
var keys = Array.isArray(window.__replayTopLevelKeys) ? window.__replayTopLevelKeys : [];
|
|
4079
|
+
var i = Number(index);
|
|
4080
|
+
if (!Number.isInteger(i) || i < 0 || i >= keys.length) return;
|
|
4081
|
+
var key = keys[i];
|
|
4082
|
+
if (typeof key !== 'string' || !key) return;
|
|
4083
|
+
|
|
4084
|
+
var obj = getReplayInputObject();
|
|
4085
|
+
try {
|
|
4086
|
+
if (kind === 'string') {
|
|
4087
|
+
obj[key] = String(value || '');
|
|
4088
|
+
} else if (kind === 'number') {
|
|
4089
|
+
var n = Number(value);
|
|
4090
|
+
if (!Number.isFinite(n)) throw new Error('invalid number');
|
|
4091
|
+
obj[key] = n;
|
|
4092
|
+
} else if (kind === 'boolean') {
|
|
4093
|
+
obj[key] = !!value;
|
|
4094
|
+
} else if (kind === 'json') {
|
|
4095
|
+
var parsed = JSON.parse(String(value || ''));
|
|
4096
|
+
obj[key] = parsed;
|
|
4097
|
+
} else if (kind === 'null') {
|
|
4098
|
+
obj[key] = null;
|
|
4099
|
+
}
|
|
4100
|
+
} catch (err) {
|
|
4101
|
+
replayState.replayInputError = String(err && err.message ? err.message : err);
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
syncReplayInputObject(obj);
|
|
4106
|
+
if (key === 'systemPrompt' || key === 'extraSystemPrompt') {
|
|
4107
|
+
replayState.systemPrompt = typeof obj.systemPrompt === 'string'
|
|
4108
|
+
? obj.systemPrompt
|
|
4109
|
+
: (typeof obj.extraSystemPrompt === 'string' ? obj.extraSystemPrompt : replayState.systemPrompt);
|
|
4110
|
+
}
|
|
4111
|
+
if (key === 'userMessage' || key === 'prompt' || key === 'historyMessages' || key === 'messages') {
|
|
4112
|
+
replayState.userPrompt = replayExtractUserPromptFromInput(obj);
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
|
|
4116
|
+
window.replayChangeReplayInput = function(value) {
|
|
4117
|
+
replayState.replayInputText = String(value || '');
|
|
4118
|
+
replayState.replayInputError = '';
|
|
4119
|
+
var raw = replayState.replayInputText.trim();
|
|
4120
|
+
if (!raw) {
|
|
4121
|
+
replayState.replayInput = null;
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
try {
|
|
4125
|
+
var parsed = JSON.parse(raw);
|
|
4126
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
4127
|
+
replayState.replayInputError = tr('Replay input must be a JSON object', '回放参数必须是 JSON 对象');
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
replayState.replayInput = parsed;
|
|
4131
|
+
} catch (err) {
|
|
4132
|
+
replayState.replayInputError = String(err && err.message ? err.message : err);
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
|
|
4136
|
+
window.replayChangeNumber = function(field, value) {
|
|
4137
|
+
if (field === 'temperature') replayState.temperature = String(value || '');
|
|
4138
|
+
if (field === 'maxTokens') replayState.maxTokens = String(value || '');
|
|
4139
|
+
};
|
|
2225
4140
|
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
4141
|
+
window.replayRun = async function() {
|
|
4142
|
+
if (replayState.running) return;
|
|
4143
|
+
replayState.error = '';
|
|
4144
|
+
replayState.result = null;
|
|
4145
|
+
var model = String(replayState.model || '').trim();
|
|
4146
|
+
var replayInputObj = (replayState.replayInput && typeof replayState.replayInput === 'object')
|
|
4147
|
+
? replayState.replayInput
|
|
4148
|
+
: null;
|
|
4149
|
+
var userPrompt = replayInputObj
|
|
4150
|
+
? String(replayExtractUserPromptFromInput(replayInputObj) || '').trim()
|
|
4151
|
+
: String(replayState.userPrompt || '').trim();
|
|
4152
|
+
var systemPrompt = replayInputObj
|
|
4153
|
+
? (typeof replayInputObj.systemPrompt === 'string'
|
|
4154
|
+
? replayInputObj.systemPrompt
|
|
4155
|
+
: (typeof replayInputObj.extraSystemPrompt === 'string' ? replayInputObj.extraSystemPrompt : replayState.systemPrompt))
|
|
4156
|
+
: replayState.systemPrompt;
|
|
4157
|
+
if (!model) {
|
|
4158
|
+
replayState.error = tr('Model is required', '模型不能为空');
|
|
4159
|
+
renderReplay();
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
if (!userPrompt) {
|
|
4163
|
+
replayState.error = tr('Prompt is required', 'Prompt 不能为空');
|
|
4164
|
+
renderReplay();
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
if (replayState.replayInputError) {
|
|
4168
|
+
replayState.error = t('replay_full_input_invalid') + ': ' + replayState.replayInputError;
|
|
4169
|
+
renderReplay();
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
replayState.running = true;
|
|
4173
|
+
renderReplay();
|
|
4174
|
+
try {
|
|
4175
|
+
var payload = {
|
|
4176
|
+
providerId: replayState.providerId || undefined,
|
|
4177
|
+
model: model,
|
|
4178
|
+
systemPrompt: systemPrompt || undefined,
|
|
4179
|
+
userPrompt: userPrompt,
|
|
4180
|
+
replayInput: replayState.replayInput && typeof replayState.replayInput === 'object' ? replayState.replayInput : undefined,
|
|
4181
|
+
temperature: replayState.temperature === '' ? undefined : Number(replayState.temperature),
|
|
4182
|
+
maxTokens: replayState.maxTokens === '' ? undefined : Number(replayState.maxTokens),
|
|
4183
|
+
};
|
|
4184
|
+
replayState.result = await postApi('/llm/replay', payload);
|
|
4185
|
+
} catch (err) {
|
|
4186
|
+
replayState.error = String(err && err.message ? err.message : err);
|
|
4187
|
+
} finally {
|
|
4188
|
+
replayState.running = false;
|
|
4189
|
+
renderReplay();
|
|
4190
|
+
}
|
|
4191
|
+
};
|
|
2229
4192
|
|
|
2230
|
-
|
|
2231
|
-
|
|
4193
|
+
async function renderReplay() {
|
|
4194
|
+
app.innerHTML = renderLayout('replay', '<div class="loading">' + esc(t('loading_replay')) + '</div>');
|
|
4195
|
+
try {
|
|
4196
|
+
await ensureReplayProviders();
|
|
4197
|
+
var providers = Array.isArray(replayProvidersCache) ? replayProvidersCache : [];
|
|
4198
|
+
var models = getReplayProviderModels(replayState.providerId);
|
|
4199
|
+
var html = '';
|
|
4200
|
+
html += '<div class="section-title">' + esc(t('replay_title')) + ' <span class="count">' + esc(t('replay_subtitle')) + '</span></div>';
|
|
4201
|
+
html += '<div class="metrics-callout">';
|
|
4202
|
+
html += '<div class="metrics-callout-title">' + esc(tr('Run one-shot prompt replay inside plugin', '在插件内执行一次性 Prompt 回放')) + '</div>';
|
|
4203
|
+
html += '<div class="metrics-callout-body">' + esc(tr('This call is isolated and will not write back to active chat sessions. Useful for prompt iteration and side-by-side comparison.', '此调用是隔离执行,不会写回正在进行中的会话。适合做 Prompt 迭代与效果对比。')) + '</div>';
|
|
4204
|
+
if (replayState.sourceSessionId) {
|
|
4205
|
+
var traceLink = '#/trace/' + encodeURIComponent(replayState.sourceSessionId);
|
|
4206
|
+
html += '<div class="metrics-callout-body" style="margin-top:8px">' + esc(tr('Source trace', '来源链路')) + ': ';
|
|
4207
|
+
html += '<a href="' + traceLink + '" style="color:var(--accent);text-decoration:none">' + esc(String(replayState.sourceSessionId)) + '</a>';
|
|
4208
|
+
if (replayState.sourceCreatedAt) {
|
|
4209
|
+
html += ' · ' + esc(tr('Captured at', '采集时间')) + ': ' + esc(fmtTime(replayState.sourceCreatedAt));
|
|
4210
|
+
}
|
|
4211
|
+
html += '</div>';
|
|
2232
4212
|
}
|
|
4213
|
+
html += '</div>';
|
|
2233
4214
|
|
|
2234
|
-
html += '<div class="
|
|
2235
|
-
html += '<div class="
|
|
2236
|
-
html += '<
|
|
2237
|
-
|
|
2238
|
-
|
|
4215
|
+
html += '<div class="an-card">';
|
|
4216
|
+
html += '<div class="replay-toolbar">';
|
|
4217
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">' + esc(tr('Provider', '服务商')) + '</span><select class="replay-input" onchange="replayChangeProvider(this.value)">';
|
|
4218
|
+
providers.forEach(function(p) {
|
|
4219
|
+
var id = String(p.providerId || '');
|
|
4220
|
+
html += '<option value="' + esc(id) + '"' + (id === replayState.providerId ? ' selected' : '') + '>' + esc(id) + '</option>';
|
|
2239
4221
|
});
|
|
2240
4222
|
html += '</select></div>';
|
|
2241
|
-
html += '<div class="toolbar-field"><span class="toolbar-label">
|
|
2242
|
-
html += '<
|
|
2243
|
-
|
|
2244
|
-
html += '<option value="' +
|
|
4223
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">' + esc(tr('Model', '模型')) + '</span><input class="replay-input" list="replay-model-list" value="' + esc(replayState.model || '') + '" oninput="replayChangeModel(this.value)" placeholder="qwen3.5-plus"></div>';
|
|
4224
|
+
html += '<datalist id="replay-model-list">';
|
|
4225
|
+
models.forEach(function(m) {
|
|
4226
|
+
html += '<option value="' + esc(String(m.id || '')) + '">' + esc(String(m.name || m.id || '')) + '</option>';
|
|
2245
4227
|
});
|
|
2246
|
-
html += '</
|
|
2247
|
-
html += '<
|
|
4228
|
+
html += '</datalist>';
|
|
4229
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">Temperature</span><input class="replay-input" type="number" step="0.1" min="0" max="2" value="' + esc(replayState.temperature || '') + '" oninput="replayChangeNumber(\\'temperature\\',this.value)" placeholder="' + esc(tr('(optional)', '(可选)')) + '"></div>';
|
|
4230
|
+
html += '<div class="toolbar-field"><span class="toolbar-label">' + esc(tr('Max tokens', '最大 Token')) + '</span><input class="replay-input" type="number" min="1" max="32768" value="' + esc(replayState.maxTokens || '') + '" oninput="replayChangeNumber(\\'maxTokens\\',this.value)" placeholder="' + esc(tr('(optional)', '(可选)')) + '"></div>';
|
|
2248
4231
|
html += '</div>';
|
|
2249
4232
|
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
if (
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
4233
|
+
html += '<div class="toolbar-field" style="margin-top:12px">';
|
|
4234
|
+
html += '<span class="toolbar-label">' + esc(t('replay_full_input')) + '</span>';
|
|
4235
|
+
var replayInputObj = getReplayInputObject();
|
|
4236
|
+
var replayTopKeys = Object.keys(replayInputObj || {});
|
|
4237
|
+
window.__replayTopLevelKeys = replayTopKeys;
|
|
4238
|
+
if (replayTopKeys.length > 0) {
|
|
4239
|
+
html += '<div class="replay-kv-list">';
|
|
4240
|
+
replayTopKeys.forEach(function(k, idx) {
|
|
4241
|
+
var v = replayInputObj[k];
|
|
4242
|
+
var type = (v === null) ? 'null' : (Array.isArray(v) ? 'array' : typeof v);
|
|
4243
|
+
html += '<div class="replay-kv-item">';
|
|
4244
|
+
html += '<div class="replay-kv-key">' + esc(String(k)) + '</div>';
|
|
4245
|
+
if (type === 'string') {
|
|
4246
|
+
html += '<textarea class="replay-kv-primitive" style="min-height:80px" oninput="replayEditTopLevelValue(' + idx + ',\\'string\\',this.value)">' + esc(String(v)) + '</textarea>';
|
|
4247
|
+
} else if (type === 'number') {
|
|
4248
|
+
html += '<input class="replay-kv-primitive" type="number" value="' + esc(String(v)) + '" oninput="replayEditTopLevelValue(' + idx + ',\\'number\\',this.value)">';
|
|
4249
|
+
} else if (type === 'boolean') {
|
|
4250
|
+
html += '<label class="replay-kv-summary"><input type="checkbox" ' + (v ? 'checked' : '') + ' onchange="replayEditTopLevelValue(' + idx + ',\\'boolean\\',this.checked)"> ' + esc(tr('Boolean value', '布尔值')) + '</label>';
|
|
4251
|
+
} else if (type === 'null') {
|
|
4252
|
+
html += '<div class="replay-kv-summary">' + esc(tr('null value', 'null 值')) + '</div>';
|
|
4253
|
+
} else {
|
|
4254
|
+
var jsonTxt = '';
|
|
4255
|
+
try { jsonTxt = JSON.stringify(v, null, 2); } catch { jsonTxt = String(v); }
|
|
4256
|
+
html += '<details>';
|
|
4257
|
+
html += '<summary class="replay-kv-summary">' + esc(tr('Show value', '显示值')) + ' (' + esc(String(jsonTxt.length)) + ' chars)</summary>';
|
|
4258
|
+
html += '<textarea class="replay-kv-json" oninput="replayEditTopLevelValue(' + idx + ',\\'json\\',this.value)">' + esc(jsonTxt) + '</textarea>';
|
|
4259
|
+
html += '</details>';
|
|
4260
|
+
}
|
|
4261
|
+
html += '</div>';
|
|
4262
|
+
});
|
|
4263
|
+
html += '</div>';
|
|
4264
|
+
} else {
|
|
4265
|
+
html += '<div class="replay-caption">' + esc(tr('No top-level fields found in replay input.', '回放参数里没有可展示的顶层字段。')) + '</div>';
|
|
2269
4266
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
4267
|
+
html += '<details class="replay-raw-details">';
|
|
4268
|
+
html += '<summary>' + esc(tr('Raw JSON editor', '原始 JSON 编辑器')) + '</summary>';
|
|
4269
|
+
html += '<textarea class="replay-area" style="min-height:220px;font-family:var(--mono);font-size:12px" oninput="replayChangeReplayInput(this.value)" placeholder="{\\n \\"historyMessages\\": []\\n}">' + esc(replayState.replayInputText || '') + '</textarea>';
|
|
4270
|
+
html += '</details>';
|
|
4271
|
+
html += '<div class="replay-caption">' + esc(t('replay_full_input_hint')) + '</div>';
|
|
4272
|
+
if (replayState.replayInputError) {
|
|
4273
|
+
html += '<div class="replay-caption" style="color:var(--danger)">' + esc(t('replay_full_input_invalid')) + ': ' + esc(replayState.replayInputError) + '</div>';
|
|
2272
4274
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
4275
|
+
html += '</div>';
|
|
4276
|
+
html += '<div class="replay-actions">';
|
|
4277
|
+
html += '<button class="replay-run" onclick="replayRun()" ' + (replayState.running ? 'disabled' : '') + '>' + (replayState.running ? esc(t('replay_running')) : esc(t('run_replay'))) + '</button>';
|
|
4278
|
+
html += '</div>';
|
|
4279
|
+
if (replayState.error) {
|
|
4280
|
+
html += '<div class="empty" style="margin-bottom:10px"><div class="text">' + esc(replayState.error) + '</div></div>';
|
|
2275
4281
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
html += '<div class="
|
|
4282
|
+
if (replayState.result) {
|
|
4283
|
+
var r = replayState.result;
|
|
4284
|
+
html += '<div class="replay-meta">' + esc(tr('Provider', '服务商')) + ': ' + esc(String(r.providerId || '-')) + ' · ' + esc(tr('Model', '模型')) + ': ' + esc(String(r.model || '-')) + ' · ' + esc(tr('Latency', '耗时')) + ': ' + fmtDur(Number(r.latencyMs || 0)) + '</div>';
|
|
4285
|
+
html += '<div class="replay-meta" style="margin-top:4px;margin-bottom:8px">' + esc(tr('Tokens', 'Token')) + ': prompt ' + fmtNum(Number((r.usage && r.usage.promptTokens) || 0)) + ' · completion ' + fmtNum(Number((r.usage && r.usage.completionTokens) || 0)) + '</div>';
|
|
4286
|
+
var baselinePrompt = replayState.baselinePromptTokens != null ? Number(replayState.baselinePromptTokens) : 0;
|
|
4287
|
+
var baselineCompletion = replayState.baselineCompletionTokens != null ? Number(replayState.baselineCompletionTokens) : 0;
|
|
4288
|
+
var nowPrompt = Number((r.usage && r.usage.promptTokens) || 0);
|
|
4289
|
+
var nowCompletion = Number((r.usage && r.usage.completionTokens) || 0);
|
|
4290
|
+
var tokenDelta = (nowPrompt + nowCompletion) - (baselinePrompt + baselineCompletion);
|
|
4291
|
+
var latencyDelta = replayState.baselineLatencyMs != null ? (Number(r.latencyMs || 0) - Number(replayState.baselineLatencyMs || 0)) : null;
|
|
4292
|
+
html += '<div class="replay-caption">';
|
|
4293
|
+
html += esc(tr('Delta', '差异')) + ': ';
|
|
4294
|
+
html += esc(tr('tokens', 'Token')) + ' ' + (tokenDelta >= 0 ? '+' : '') + fmtNum(tokenDelta) + ' · ';
|
|
4295
|
+
html += esc(tr('latency', '耗时')) + ' ' + (latencyDelta == null ? '-' : ((latencyDelta >= 0 ? '+' : '') + fmtDur(latencyDelta)));
|
|
4296
|
+
html += '</div>';
|
|
4297
|
+
html += '<div class="replay-compare">';
|
|
4298
|
+
html += '<div><div class="replay-caption">' + esc(tr('Baseline output (from trace)', '基线输出(来自 Trace)')) + '</div><div class="replay-result">' + esc(String(replayState.baselineText || '')) + '</div></div>';
|
|
4299
|
+
html += '<div><div class="replay-caption">' + esc(tr('Replay output (current config)', '回放输出(当前配置)')) + '</div><div class="replay-result">' + esc(String(r.text || '')) + '</div></div>';
|
|
4300
|
+
html += '</div>';
|
|
2280
4301
|
} else {
|
|
2281
|
-
|
|
4302
|
+
html += '<div class="replay-result" style="color:var(--muted)">' + esc(t('no_replay_result')) + '</div>';
|
|
2282
4303
|
}
|
|
2283
4304
|
html += '</div>';
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
var enc = encodeURIComponent(nm);
|
|
2289
|
-
var s = metricSeriesMap[nm] || { points: [], metricType: m.metricType || 'untyped' };
|
|
2290
|
-
html += '<div class="an-card" style="margin-bottom:16px" data-spark-metric="' + enc + '">';
|
|
2291
|
-
html += '<h3><span class="icon">📈</span> ' + esc(nm) + '<span class="sub">' + esc(s.metricType || m.metricType || 'untyped') + ' · ' + metricsStepSec + 's buckets</span></h3>';
|
|
2292
|
-
html += buildMetricSparkline((s && s.points) || [], metricsChartDomain);
|
|
2293
|
-
html += '</div>';
|
|
2294
|
-
});
|
|
2295
|
-
|
|
2296
|
-
html += '<div class="an-card">';
|
|
2297
|
-
html += '<h3><span class="icon">🧾</span> Latest Metrics Snapshot <span class="sub">sum across labels · click a row to jump to its chart</span></h3>';
|
|
2298
|
-
html += '<div class="metrics-snapshot-tools">';
|
|
2299
|
-
html += '<input type="search" placeholder="Filter by metric name…" aria-label="Filter metrics table" value="' + esc(metricsSnapshotFilter) + '" oninput="metricsTableFilterInput(this.value)" />';
|
|
2300
|
-
html += '<div class="toolbar-field"><span class="toolbar-label">Sort</span>';
|
|
2301
|
-
html += '<select class="metrics-select" aria-label="Sort metrics table" onchange="metricsSortChange(this.value)">';
|
|
2302
|
-
html += '<option value="name"' + (metricsTableSort==='name'?' selected':'') + '>Name (A–Z)</option>';
|
|
2303
|
-
html += '<option value="samples"' + (metricsTableSort==='samples'?' selected':'') + '>Samples (high → low)</option>';
|
|
2304
|
-
html += '<option value="recent"' + (metricsTableSort==='recent'?' selected':'') + '>Latest time</option>';
|
|
2305
|
-
html += '</select></div></div>';
|
|
2306
|
-
html += '<div class="an-table-scroll"><table class="an-table" id="metrics-snapshot-table">';
|
|
2307
|
-
html += '<thead><tr><th>Metric</th><th>Type</th><th>Latest Value</th><th>Latest Timestamp</th><th>Samples</th></tr></thead>';
|
|
2308
|
-
html += '<tbody id="metrics-snapshot-tbody">';
|
|
2309
|
-
itemsDisplay.forEach(function(m) {
|
|
2310
|
-
var enc = encodeURIComponent(m.metricName);
|
|
2311
|
-
html += '<tr class="metrics-row' + (m.metricName===metricsSelected?' metrics-row-active':'') + '" data-m="' + enc + '" role="button" tabindex="0" aria-label="Jump to chart for ' + esc(m.metricName) + '" title="Jump to chart (click or Enter)">';
|
|
2312
|
-
html += '<td class="mono">' + esc(m.metricName) + '</td>';
|
|
2313
|
-
html += '<td>' + esc(m.metricType || 'untyped') + '</td>';
|
|
2314
|
-
html += '<td class="mono">' + fmtNum(Number(m.latestValue || 0)) + '</td>';
|
|
2315
|
-
html += '<td class="mono">' + (m.latestTimestampMs ? fmtTime(m.latestTimestampMs) : '-') + '</td>';
|
|
2316
|
-
html += '<td class="mono">' + fmtNum(Number(m.samples || 0)) + '</td>';
|
|
2317
|
-
html += '</tr>';
|
|
2318
|
-
});
|
|
2319
|
-
html += '</tbody></table></div>';
|
|
2320
|
-
html += '</div>';
|
|
2321
|
-
|
|
2322
|
-
app.innerHTML = renderLayout('metrics', html);
|
|
2323
|
-
requestAnimationFrame(function() { metricsTableFilterInput(metricsSnapshotFilter); });
|
|
2324
|
-
} catch(err) {
|
|
2325
|
-
app.innerHTML = renderLayout('metrics',
|
|
2326
|
-
'<div class="empty"><div class="icon">⚠️</div><div class="text">Failed to load metrics: ' + esc(String(err)) + '</div></div>');
|
|
4305
|
+
app.innerHTML = renderLayout('replay', html);
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
app.innerHTML = renderLayout('replay',
|
|
4308
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>');
|
|
2327
4309
|
}
|
|
2328
4310
|
}
|
|
2329
4311
|
|
|
@@ -2332,14 +4314,14 @@ async function renderMetrics() {
|
|
|
2332
4314
|
function buildTimeSeriesChart(ts, metric, gran) {
|
|
2333
4315
|
gran = gran || 'day';
|
|
2334
4316
|
if (!ts || ts.length === 0) {
|
|
2335
|
-
return '<div class="empty" style="padding:24px"><div class="text">No data in selected range</div></div>';
|
|
4317
|
+
return '<div class="empty" style="padding:24px"><div class="text">' + esc(tr('No data in selected range', '当前时间范围无数据')) + '</div></div>';
|
|
2336
4318
|
}
|
|
2337
4319
|
|
|
2338
4320
|
// Metric tab switcher
|
|
2339
4321
|
var h = '<div class="metric-tabs">';
|
|
2340
|
-
h += '<div class="metric-tab' + (metric==='sessions'?' active':'') + '" onclick="anSwitchMetric(\\'sessions\\')">Sessions</div>';
|
|
2341
|
-
h += '<div class="metric-tab' + (metric==='tokens'?' active':'') + '" onclick="anSwitchMetric(\\'tokens\\')">Tokens</div>';
|
|
2342
|
-
h += '<div class="metric-tab' + (metric==='actions'?' active':'') + '" onclick="anSwitchMetric(\\'actions\\')">Actions</div>';
|
|
4322
|
+
h += '<div class="metric-tab' + (metric==='sessions'?' active':'') + '" onclick="anSwitchMetric(\\'sessions\\')">' + esc(tr('Sessions', '会话')) + '</div>';
|
|
4323
|
+
h += '<div class="metric-tab' + (metric==='tokens'?' active':'') + '" onclick="anSwitchMetric(\\'tokens\\')">' + esc(tr('Tokens', 'Token')) + '</div>';
|
|
4324
|
+
h += '<div class="metric-tab' + (metric==='actions'?' active':'') + '" onclick="anSwitchMetric(\\'actions\\')">' + esc(tr('Actions', '动作')) + '</div>';
|
|
2343
4325
|
h += '</div>';
|
|
2344
4326
|
|
|
2345
4327
|
// Determine values and max
|
|
@@ -2398,7 +4380,7 @@ function buildTimeSeriesChart(ts, metric, gran) {
|
|
|
2398
4380
|
var pct = Math.max((v.total / maxVal) * 100, 1);
|
|
2399
4381
|
var fullLabel = formatFullTimeLabel(v.label);
|
|
2400
4382
|
h += '<div class="chart-bar-col">';
|
|
2401
|
-
h += '<div class="chart-tooltip"><div><b>
|
|
4383
|
+
h += '<div class="chart-tooltip"><div><b>' + esc(t('time')) + ':</b> ' + esc(fullLabel) + '</div><div><b>' + esc(tr('Value', '值')) + ':</b> ' + fmtNum(v.total);
|
|
2402
4384
|
if (showTokenSplit) h += ' (in:' + fmtNum(v.v1) + ' out:' + fmtNum(v.v2) + ')';
|
|
2403
4385
|
h += '</div></div>';
|
|
2404
4386
|
|
|
@@ -2430,8 +4412,8 @@ function buildTimeSeriesChart(ts, metric, gran) {
|
|
|
2430
4412
|
// Legend for token split
|
|
2431
4413
|
if (showTokenSplit) {
|
|
2432
4414
|
h += '<div class="chart-legend">';
|
|
2433
|
-
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#8b5cf6"></div>Input tokens</div>';
|
|
2434
|
-
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#f59e0b"></div>Output tokens</div>';
|
|
4415
|
+
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#8b5cf6"></div>' + esc(tr('Input tokens', '输入 Token')) + '</div>';
|
|
4416
|
+
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#f59e0b"></div>' + esc(tr('Output tokens', '输出 Token')) + '</div>';
|
|
2435
4417
|
h += '</div>';
|
|
2436
4418
|
}
|
|
2437
4419
|
|
|
@@ -2440,7 +4422,7 @@ function buildTimeSeriesChart(ts, metric, gran) {
|
|
|
2440
4422
|
|
|
2441
4423
|
function buildModelUsageChart(mu, tbm) {
|
|
2442
4424
|
if (!mu || mu.length === 0) {
|
|
2443
|
-
return '<div class="empty" style="padding:24px"><div class="text">No model data</div></div>';
|
|
4425
|
+
return '<div class="empty" style="padding:24px"><div class="text">' + esc(tr('No model data', '暂无模型数据')) + '</div></div>';
|
|
2444
4426
|
}
|
|
2445
4427
|
|
|
2446
4428
|
var maxTokens = Math.max.apply(null, mu.map(function(m){ return m.inputTokens + m.outputTokens; }));
|
|
@@ -2465,13 +4447,13 @@ function buildModelUsageChart(mu, tbm) {
|
|
|
2465
4447
|
h += '</div>';
|
|
2466
4448
|
|
|
2467
4449
|
h += '<div class="chart-legend" style="margin-top:16px">';
|
|
2468
|
-
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#8b5cf6"></div>Input</div>';
|
|
2469
|
-
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#f59e0b"></div>Output</div>';
|
|
4450
|
+
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#8b5cf6"></div>' + esc(tr('Input', '输入')) + '</div>';
|
|
4451
|
+
h += '<div class="chart-legend-item"><div class="chart-legend-dot" style="background:#f59e0b"></div>' + esc(tr('Output', '输出')) + '</div>';
|
|
2470
4452
|
h += '</div>';
|
|
2471
4453
|
|
|
2472
4454
|
// Model details table
|
|
2473
4455
|
h += '<table class="an-table" style="margin-top:16px">';
|
|
2474
|
-
h += '<tr><th>Model</th><th>Calls</th><th>Avg Latency</th><th>Tokens</th></tr>';
|
|
4456
|
+
h += '<tr><th>' + esc(tr('Model', '模型')) + '</th><th>' + esc(tr('Calls', '调用次数')) + '</th><th>' + esc(tr('Avg Latency', '平均耗时')) + '</th><th>' + esc(tr('Tokens', 'Token')) + '</th></tr>';
|
|
2475
4457
|
mu.forEach(function(m) {
|
|
2476
4458
|
var shortModel = m.model.length > 20 ? m.model.slice(m.model.indexOf('/') + 1) : m.model;
|
|
2477
4459
|
h += '<tr>';
|
|
@@ -2489,7 +4471,7 @@ function buildModelUsageChart(mu, tbm) {
|
|
|
2489
4471
|
function buildActionDistribution(ad) {
|
|
2490
4472
|
var keys = Object.keys(ad);
|
|
2491
4473
|
if (keys.length === 0) {
|
|
2492
|
-
return '<div class="empty" style="padding:24px"><div class="text">No action data</div></div>';
|
|
4474
|
+
return '<div class="empty" style="padding:24px"><div class="text">' + esc(tr('No action data', '暂无动作数据')) + '</div></div>';
|
|
2493
4475
|
}
|
|
2494
4476
|
|
|
2495
4477
|
var total = keys.reduce(function(s, k) { return s + ad[k]; }, 0);
|
|
@@ -2548,11 +4530,11 @@ function buildAgentsTable(ta) {
|
|
|
2548
4530
|
});
|
|
2549
4531
|
|
|
2550
4532
|
if (!rows || rows.length === 0) {
|
|
2551
|
-
return '<div class="empty" style="padding:24px"><div class="text">No agent data</div></div>';
|
|
4533
|
+
return '<div class="empty" style="padding:24px"><div class="text">' + esc(tr('No agent data', '暂无代理数据')) + '</div></div>';
|
|
2552
4534
|
}
|
|
2553
4535
|
|
|
2554
4536
|
var h = '<table class="an-table">';
|
|
2555
|
-
h += '<tr><th>Agent</th><th>Sessions</th><th>Actions</th><th>Tokens</th></tr>';
|
|
4537
|
+
h += '<tr><th>' + esc(tr('Agent', '代理')) + '</th><th>' + esc(tr('Sessions', '会话')) + '</th><th>' + esc(tr('Actions', '动作')) + '</th><th>' + esc(tr('Tokens', 'Token')) + '</th></tr>';
|
|
2556
4538
|
rows.forEach(function(a) {
|
|
2557
4539
|
h += '<tr>';
|
|
2558
4540
|
h += '<td>🤖 ' + esc(a.agent) + '</td>';
|
|
@@ -2635,17 +4617,48 @@ var secActiveRuleKey = 'secretLeakage';
|
|
|
2635
4617
|
/* ---- severity helpers ---- */
|
|
2636
4618
|
var SEV_ICON = {critical:'🔴',warn:'🟡',info:'ℹ️'};
|
|
2637
4619
|
var SEV_LABEL = {critical:'CRITICAL',warn:'WARNING',info:'INFO'};
|
|
2638
|
-
|
|
2639
|
-
secret_leakage
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
4620
|
+
function secCategoryLabel(cat) {
|
|
4621
|
+
if (cat === 'secret_leakage') return tr('Secret Leakage', '密钥泄露');
|
|
4622
|
+
if (cat === 'high_risk_operation') return tr('High Risk Operation', '高风险操作');
|
|
4623
|
+
if (cat === 'data_exfiltration') return tr('Data Exfiltration', '数据外传');
|
|
4624
|
+
if (cat === 'prompt_injection') return tr('Prompt Injection', '提示词注入');
|
|
4625
|
+
if (cat === 'custom_regex') return tr('Custom Regex', '自定义规则');
|
|
4626
|
+
if (cat === 'skill_anomaly') return tr('Skill Anomaly', '技能异常');
|
|
4627
|
+
return String(cat || '-');
|
|
4628
|
+
}
|
|
4629
|
+
function secStatusLabel(status) {
|
|
4630
|
+
if (status === 'open') return tr('Open', '待处理');
|
|
4631
|
+
if (status === 'acknowledged') return tr('Acknowledged', '已确认');
|
|
4632
|
+
if (status === 'resolved') return tr('Resolved', '已解决');
|
|
4633
|
+
return String(status || '-');
|
|
4634
|
+
}
|
|
4635
|
+
function secSeverityOptions() {
|
|
4636
|
+
return [
|
|
4637
|
+
{k:'', l: tr('All Severity', '全部严重级别')},
|
|
4638
|
+
{k:'critical', l:'CRITICAL'},
|
|
4639
|
+
{k:'warn', l:'WARNING'},
|
|
4640
|
+
{k:'info', l:'INFO'}
|
|
4641
|
+
];
|
|
4642
|
+
}
|
|
4643
|
+
function secCategoryOptions() {
|
|
4644
|
+
return [
|
|
4645
|
+
{k:'', l: tr('All Category', '全部类别')},
|
|
4646
|
+
{k:'secret_leakage', l: secCategoryLabel('secret_leakage')},
|
|
4647
|
+
{k:'high_risk_operation', l: secCategoryLabel('high_risk_operation')},
|
|
4648
|
+
{k:'data_exfiltration', l: secCategoryLabel('data_exfiltration')},
|
|
4649
|
+
{k:'prompt_injection', l: secCategoryLabel('prompt_injection')},
|
|
4650
|
+
{k:'custom_regex', l: secCategoryLabel('custom_regex')},
|
|
4651
|
+
{k:'skill_anomaly', l: secCategoryLabel('skill_anomaly')}
|
|
4652
|
+
];
|
|
4653
|
+
}
|
|
4654
|
+
function secStatusOptions() {
|
|
4655
|
+
return [
|
|
4656
|
+
{k:'', l: tr('All Status', '全部状态')},
|
|
4657
|
+
{k:'open', l: secStatusLabel('open')},
|
|
4658
|
+
{k:'acknowledged', l: secStatusLabel('acknowledged')},
|
|
4659
|
+
{k:'resolved', l: secStatusLabel('resolved')}
|
|
4660
|
+
];
|
|
4661
|
+
}
|
|
2649
4662
|
var SEC_RULE_META = {
|
|
2650
4663
|
secretLeakage: {
|
|
2651
4664
|
key: 'secretLeakage',
|
|
@@ -2713,9 +4726,9 @@ function secGetTimeFromISO() {
|
|
|
2713
4726
|
}
|
|
2714
4727
|
|
|
2715
4728
|
function secGetTimeLabel() {
|
|
2716
|
-
if (!secFilterTimeRange) return 'All time';
|
|
4729
|
+
if (!secFilterTimeRange) return currentLang === 'zh' ? '全部时间' : 'All time';
|
|
2717
4730
|
var preset = TIME_PRESETS.find(function(p){ return p.key === secFilterTimeRange; });
|
|
2718
|
-
return preset ? preset
|
|
4731
|
+
return preset ? timePresetLabel(preset) : (currentLang === 'zh' ? '全部时间' : 'All time');
|
|
2719
4732
|
}
|
|
2720
4733
|
|
|
2721
4734
|
function secSelectedLabel(opts, curKey) {
|
|
@@ -2733,6 +4746,80 @@ function secSafeActiveRuleKey() {
|
|
|
2733
4746
|
if (SEC_RULE_ORDER.indexOf(secActiveRuleKey) >= 0) return secActiveRuleKey;
|
|
2734
4747
|
return 'secretLeakage';
|
|
2735
4748
|
}
|
|
4749
|
+
function secRuleCopy(ruleKey, meta) {
|
|
4750
|
+
if (ruleKey === 'secretLeakage') {
|
|
4751
|
+
return {
|
|
4752
|
+
title: tr('Secret Leakage', '密钥泄露'),
|
|
4753
|
+
brief: tr('Detect exposed AK/SK, token, password and key materials', '检测 AK/SK、Token、密码与密钥材料外泄'),
|
|
4754
|
+
description: tr('Scans tool input/output and model responses for secret patterns, including keys, tokens and sensitive credential strings.', '扫描工具输入输出与模型响应中的密钥模式,包括 key、token 与敏感凭证字符串。'),
|
|
4755
|
+
detects: [
|
|
4756
|
+
tr('Hardcoded access key or token values', '硬编码访问密钥或 Token'),
|
|
4757
|
+
tr('Credential output copied from files or env blocks', '从文件或环境变量中泄露的凭证内容')
|
|
4758
|
+
]
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
if (ruleKey === 'highRiskOps') {
|
|
4762
|
+
return {
|
|
4763
|
+
title: tr('High Risk Ops', '高风险操作'),
|
|
4764
|
+
brief: tr('Detect dangerous operations such as delete/reset/wipe', '检测删除/重置/清空等危险操作'),
|
|
4765
|
+
description: tr('Flags risky tool operations that may alter or destroy data, especially shell and file-system actions.', '标记可能导致数据被修改或删除的高风险工具操作,重点覆盖 shell 与文件系统动作。'),
|
|
4766
|
+
detects: [
|
|
4767
|
+
tr('Destructive shell commands', '破坏性 shell 命令'),
|
|
4768
|
+
tr('Irreversible write/remove actions', '不可逆写入/删除动作')
|
|
4769
|
+
]
|
|
4770
|
+
};
|
|
4771
|
+
}
|
|
4772
|
+
if (ruleKey === 'dataExfiltration') {
|
|
4773
|
+
return {
|
|
4774
|
+
title: tr('External Access', '外部访问'),
|
|
4775
|
+
brief: tr('Detect outbound requests and potential data exfiltration', '检测外发请求与潜在数据外传'),
|
|
4776
|
+
description: tr('Monitors requests to external websites or domains and raises alerts when outbound access may carry sensitive data.', '监控对外部域名的访问,在可能携带敏感数据时触发告警。'),
|
|
4777
|
+
detects: [
|
|
4778
|
+
tr('Tool calls to non-whitelisted external domains', '访问非白名单外部域名的工具调用'),
|
|
4779
|
+
tr('Potential outbound data transfer patterns', '疑似数据外传行为模式')
|
|
4780
|
+
]
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
if (ruleKey === 'customRegex') {
|
|
4784
|
+
return {
|
|
4785
|
+
title: tr('Custom Regex', '自定义正则'),
|
|
4786
|
+
brief: tr('Customer-defined regex rules with live matching', '支持用户自定义正则并实时匹配'),
|
|
4787
|
+
description: tr('Create your own regex rules for sensitive keywords, IDs, or organization-specific policy patterns.', '可针对敏感关键词、ID 或组织内部策略模式配置自定义规则。'),
|
|
4788
|
+
detects: [
|
|
4789
|
+
tr('Custom compliance vocabulary', '自定义合规词汇'),
|
|
4790
|
+
tr('Business-specific sensitive identifiers', '业务特有敏感标识')
|
|
4791
|
+
]
|
|
4792
|
+
};
|
|
4793
|
+
}
|
|
4794
|
+
if (ruleKey === 'promptInjection') {
|
|
4795
|
+
return {
|
|
4796
|
+
title: tr('Prompt Injection', '提示词注入'),
|
|
4797
|
+
brief: tr('Detect jailbreak or instruction override attempts', '检测越狱与指令覆盖尝试'),
|
|
4798
|
+
description: tr('Finds suspicious prompts attempting to override system policy, bypass guardrails or exfiltrate hidden context.', '识别尝试覆盖系统策略、绕过护栏或套取隐藏上下文的可疑提示词。'),
|
|
4799
|
+
detects: [
|
|
4800
|
+
tr('Ignore previous instructions patterns', '忽略先前指令类模式'),
|
|
4801
|
+
tr('Role hijacking and policy bypass phrasing', '角色劫持与策略绕过语句')
|
|
4802
|
+
]
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
if (ruleKey === 'chainDetection') {
|
|
4806
|
+
return {
|
|
4807
|
+
title: tr('Chain Detection', '链路组合检测'),
|
|
4808
|
+
brief: tr('Detect suspicious multi-step attack chains', '检测可疑多步骤攻击链'),
|
|
4809
|
+
description: tr('Correlates multiple actions in a session and raises alerts for risky sequences that are mild alone but dangerous in combination.', '关联会话内多步动作,对单步风险低但组合后危险的序列进行告警。'),
|
|
4810
|
+
detects: [
|
|
4811
|
+
tr('Recon + secret read + external write pattern', '侦察 + 读取敏感信息 + 外部写出模式'),
|
|
4812
|
+
tr('Repeated escalations over short window', '短时间内重复升级操作')
|
|
4813
|
+
]
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4816
|
+
return {
|
|
4817
|
+
title: String(meta && meta.title || ruleKey),
|
|
4818
|
+
brief: String(meta && meta.brief || ''),
|
|
4819
|
+
description: String(meta && meta.description || ''),
|
|
4820
|
+
detects: Array.isArray(meta && meta.detects) ? meta.detects.slice() : []
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
2736
4823
|
|
|
2737
4824
|
/* build one custom dropdown (same look as Dashboard time picker) */
|
|
2738
4825
|
function secDropdown(id, icon, opts, curKey) {
|
|
@@ -2761,6 +4848,7 @@ function buildSecurityRuntimeHtml() {
|
|
|
2761
4848
|
|
|
2762
4849
|
var activeRuleKey = secSafeActiveRuleKey();
|
|
2763
4850
|
var activeMeta = SEC_RULE_META[activeRuleKey] || SEC_RULE_META.secretLeakage;
|
|
4851
|
+
var activeCopy = secRuleCopy(activeRuleKey, activeMeta);
|
|
2764
4852
|
var activeOn = secRuleEnabled(activeRuleKey);
|
|
2765
4853
|
var enabledRules = SEC_RULE_ORDER.filter(function(k){
|
|
2766
4854
|
return secRuleEnabled(k);
|
|
@@ -2771,9 +4859,9 @@ function buildSecurityRuntimeHtml() {
|
|
|
2771
4859
|
html += '<div class="sec-runtime">';
|
|
2772
4860
|
html += '<div class="sec-runtime-head">';
|
|
2773
4861
|
html += '<div>';
|
|
2774
|
-
html += '<div class="sec-runtime-title">Runtime Security Rules</div>';
|
|
4862
|
+
html += '<div class="sec-runtime-title">' + esc(tr('Runtime Security Rules', '运行时安全规则')) + '</div>';
|
|
2775
4863
|
html += '</div>';
|
|
2776
|
-
html += '<div class="sec-runtime-summary">' + enabledRules + '/' + SEC_RULE_ORDER.length + ' rule(s) enabled</div>';
|
|
4864
|
+
html += '<div class="sec-runtime-summary">' + enabledRules + '/' + SEC_RULE_ORDER.length + ' ' + esc(tr('rule(s) enabled', '条规则已启用')) + '</div>';
|
|
2777
4865
|
html += '</div>';
|
|
2778
4866
|
|
|
2779
4867
|
html += '<div class="sec-runtime-grid">';
|
|
@@ -2781,14 +4869,15 @@ function buildSecurityRuntimeHtml() {
|
|
|
2781
4869
|
SEC_RULE_ORDER.forEach(function(ruleKey) {
|
|
2782
4870
|
var meta = SEC_RULE_META[ruleKey];
|
|
2783
4871
|
if (!meta) return;
|
|
4872
|
+
var copy = secRuleCopy(ruleKey, meta);
|
|
2784
4873
|
var on = secRuleEnabled(ruleKey);
|
|
2785
4874
|
html += '<div class="sec-rule-item' + (ruleKey === activeRuleKey ? ' active' : '') + '" onclick="secSelectRule(event,\\'' + ruleKey + '\\')">';
|
|
2786
4875
|
html += '<div class="sec-rule-copy">';
|
|
2787
|
-
html += '<div class="sec-rule-name">' + esc(
|
|
2788
|
-
html += '<div class="sec-rule-brief">' + esc(
|
|
4876
|
+
html += '<div class="sec-rule-name">' + esc(copy.title) + '</div>';
|
|
4877
|
+
html += '<div class="sec-rule-brief">' + esc(copy.brief) + '</div>';
|
|
2789
4878
|
html += '</div>';
|
|
2790
|
-
html += '<span class="sec-state-pill ' + (on ? 'on' : 'off') + '">' + (on ? 'ON' : 'OFF') + '</span>';
|
|
2791
|
-
html += '<button class="sec-switch ' + (on ? 'on' : 'off') + '" onclick="event.stopPropagation();secToggleRuntime(\\'' + ruleKey + '\\')" aria-label="Toggle ' + esc(
|
|
4879
|
+
html += '<span class="sec-state-pill ' + (on ? 'on' : 'off') + '">' + (on ? tr('ON', '开') : tr('OFF', '关')) + '</span>';
|
|
4880
|
+
html += '<button class="sec-switch ' + (on ? 'on' : 'off') + '" onclick="event.stopPropagation();secToggleRuntime(\\'' + ruleKey + '\\')" aria-label="' + esc(tr('Toggle', '切换')) + ' ' + esc(copy.title) + '">';
|
|
2792
4881
|
html += '<span class="sec-switch-track"></span><span class="sec-switch-thumb"></span>';
|
|
2793
4882
|
html += '</button>';
|
|
2794
4883
|
html += '</div>';
|
|
@@ -2797,20 +4886,20 @@ function buildSecurityRuntimeHtml() {
|
|
|
2797
4886
|
|
|
2798
4887
|
html += '<div class="sec-rule-detail">';
|
|
2799
4888
|
html += '<div class="sec-rule-detail-title">';
|
|
2800
|
-
html += '<h4>' + esc(
|
|
2801
|
-
html += '<span class="sec-state-pill ' + (activeOn ? 'on' : 'off') + '">' + (activeOn ? 'ON' : 'OFF') + '</span>';
|
|
4889
|
+
html += '<h4>' + esc(activeCopy.title) + '</h4>';
|
|
4890
|
+
html += '<span class="sec-state-pill ' + (activeOn ? 'on' : 'off') + '">' + (activeOn ? tr('ON', '开') : tr('OFF', '关')) + '</span>';
|
|
2802
4891
|
html += '</div>';
|
|
2803
4892
|
html += '<div class="sec-rule-detail-meta">';
|
|
2804
|
-
html += '<span class="sec-meta-chip level-' + esc(activeMeta.level) + '">Risk ' + esc(activeMeta.level || 'unknown') + '</span>';
|
|
4893
|
+
html += '<span class="sec-meta-chip level-' + esc(activeMeta.level) + '">' + esc(tr('Risk', '风险')) + ' ' + esc(activeMeta.level || tr('unknown', '未知')) + '</span>';
|
|
2805
4894
|
if (activeRuleKey === 'customRegex') {
|
|
2806
|
-
html += '<span class="sec-meta-chip">Severity Fixed: CRITICAL</span>';
|
|
4895
|
+
html += '<span class="sec-meta-chip">' + esc(tr('Severity Fixed: CRITICAL', '严重级别固定:CRITICAL')) + '</span>';
|
|
2807
4896
|
}
|
|
2808
4897
|
html += '</div>';
|
|
2809
4898
|
if (activeRuleKey !== 'customRegex') {
|
|
2810
|
-
html += '<div class="sec-rule-explain"><div class="sec-rule-explain-text">' + esc(
|
|
4899
|
+
html += '<div class="sec-rule-explain"><div class="sec-rule-explain-text">' + esc(activeCopy.description || tr('No description', '暂无说明')) + '</div></div>';
|
|
2811
4900
|
html += '<div class="sec-rule-detail-list">';
|
|
2812
|
-
html += '<div class="sec-rule-detail-list-title">Coverage examples</div>';
|
|
2813
|
-
(
|
|
4901
|
+
html += '<div class="sec-rule-detail-list-title">' + esc(tr('Coverage examples', '检测示例')) + '</div>';
|
|
4902
|
+
(activeCopy.detects || []).forEach(function(item){
|
|
2814
4903
|
html += '<div class="sec-rule-detail-list-item">* ' + esc(item) + '</div>';
|
|
2815
4904
|
});
|
|
2816
4905
|
html += '</div>';
|
|
@@ -2818,22 +4907,22 @@ function buildSecurityRuntimeHtml() {
|
|
|
2818
4907
|
|
|
2819
4908
|
if (activeRuleKey === 'customRegex') {
|
|
2820
4909
|
html += '<div class="sec-custom-editor">';
|
|
2821
|
-
html += '<div class="sec-rule-detail-list-title">Add custom regex rule</div>';
|
|
4910
|
+
html += '<div class="sec-rule-detail-list-title">' + esc(tr('Add custom regex rule', '新增自定义正则规则')) + '</div>';
|
|
2822
4911
|
html += '<div class="sec-custom-form">';
|
|
2823
4912
|
html += '<div class="sec-custom-field">';
|
|
2824
|
-
html += '<div class="sec-custom-field-label">Rule Name</div>';
|
|
2825
|
-
html += '<input id="sec-cr-name" type="text" placeholder="e.g. Internal Ticket ID">';
|
|
4913
|
+
html += '<div class="sec-custom-field-label">' + esc(tr('Rule Name', '规则名称')) + '</div>';
|
|
4914
|
+
html += '<input id="sec-cr-name" type="text" placeholder="' + esc(tr('e.g. Internal Ticket ID', '例如:内部工单 ID')) + '">';
|
|
2826
4915
|
html += '</div>';
|
|
2827
4916
|
html += '<div class="sec-custom-field">';
|
|
2828
|
-
html += '<div class="sec-custom-field-label">Regex Pattern</div>';
|
|
2829
|
-
html += '<input id="sec-cr-pattern" type="text" placeholder="e.g. TKT-[0-9]{6}">';
|
|
4917
|
+
html += '<div class="sec-custom-field-label">' + esc(tr('Regex Pattern', '正则表达式')) + '</div>';
|
|
4918
|
+
html += '<input id="sec-cr-pattern" type="text" placeholder="' + esc(tr('e.g. TKT-[0-9]{6}', '例如:TKT-[0-9]{6}')) + '">';
|
|
2830
4919
|
html += '</div>';
|
|
2831
|
-
html += '<button onclick="secAddCustomRegex()">+ Create Rule</button>';
|
|
4920
|
+
html += '<button onclick="secAddCustomRegex()">+ ' + esc(tr('Create Rule', '创建规则')) + '</button>';
|
|
2832
4921
|
html += '</div>';
|
|
2833
4922
|
|
|
2834
4923
|
html += '<div class="sec-custom-list">';
|
|
2835
4924
|
if (customRegexRules.length === 0) {
|
|
2836
|
-
html += '<div class="sec-rule-detail-list-item">No custom regex rules yet
|
|
4925
|
+
html += '<div class="sec-rule-detail-list-item">' + esc(tr('No custom regex rules yet.', '暂无自定义正则规则。')) + '</div>';
|
|
2837
4926
|
} else {
|
|
2838
4927
|
customRegexRules.forEach(function(rule, idx){
|
|
2839
4928
|
var on = rule.enabled !== false;
|
|
@@ -2845,7 +4934,7 @@ function buildSecurityRuntimeHtml() {
|
|
|
2845
4934
|
html += '</div>';
|
|
2846
4935
|
html += '<div class="sec-custom-item-actions">';
|
|
2847
4936
|
html += '<button class="sec-switch ' + (on ? 'on' : 'off') + '" onclick="secToggleCustomRegexRule(' + idx + ')"><span class="sec-switch-track"></span><span class="sec-switch-thumb"></span></button>';
|
|
2848
|
-
html += '<button class="sec-custom-item-del" title="Delete rule" onclick="secDeleteCustomRegexRule(' + idx + ')">Delete</button>';
|
|
4937
|
+
html += '<button class="sec-custom-item-del" title="' + esc(tr('Delete rule', '删除规则')) + '" onclick="secDeleteCustomRegexRule(' + idx + ')">' + esc(tr('Delete', '删除')) + '</button>';
|
|
2849
4938
|
html += '</div>';
|
|
2850
4939
|
html += '</div>';
|
|
2851
4940
|
});
|
|
@@ -2875,7 +4964,7 @@ function renderSecurityRuntimeOnly() {
|
|
|
2875
4964
|
}
|
|
2876
4965
|
|
|
2877
4966
|
async function renderSecurity() {
|
|
2878
|
-
app.innerHTML = renderLayout('security', '<div class="loading">Loading security alerts
|
|
4967
|
+
app.innerHTML = renderLayout('security', '<div class="loading">' + esc(tr('Loading security alerts...', '加载安全告警中...')) + '</div>');
|
|
2879
4968
|
|
|
2880
4969
|
try {
|
|
2881
4970
|
var qs = 'page=' + secPage + '&limit=20';
|
|
@@ -2907,11 +4996,11 @@ async function renderSecurity() {
|
|
|
2907
4996
|
var totalAlerts = alertStats.total || 0;
|
|
2908
4997
|
|
|
2909
4998
|
html += '<div class="stat-grid">';
|
|
2910
|
-
html += statCard('Total Alerts', String(totalAlerts));
|
|
4999
|
+
html += statCard(tr('Total Alerts', '告警总数'), String(totalAlerts));
|
|
2911
5000
|
html += '<div class="stat"><div class="stat-label">Critical</div><div class="stat-value ' + (critCount>0?'val-critical':'') + '">' + critCount + '</div></div>';
|
|
2912
|
-
html += '<div class="stat"><div class="stat-label">Warnings</div><div class="stat-value ' + (warnCount>0?'val-warn':'') + '">' + warnCount + '</div></div>';
|
|
2913
|
-
html += '<div class="stat"><div class="stat-label">
|
|
2914
|
-
html += statCard('Last 24h', String(alertStats.recent24h || 0));
|
|
5001
|
+
html += '<div class="stat"><div class="stat-label">' + esc(tr('Warnings', '警告')) + '</div><div class="stat-value ' + (warnCount>0?'val-warn':'') + '">' + warnCount + '</div></div>';
|
|
5002
|
+
html += '<div class="stat"><div class="stat-label">' + esc(secStatusLabel('open')) + '</div><div class="stat-value ' + (openCount>0?'val-critical':'val-ok') + '" id="sec-open-count">' + openCount + '</div></div>';
|
|
5003
|
+
html += statCard(tr('Last 24h', '近 24 小时'), String(alertStats.recent24h || 0));
|
|
2915
5004
|
html += '</div>';
|
|
2916
5005
|
|
|
2917
5006
|
// --- Runtime security rule toggles ---
|
|
@@ -2920,20 +5009,20 @@ async function renderSecurity() {
|
|
|
2920
5009
|
// --- Filter bar (same .filter-bar as Dashboard) ---
|
|
2921
5010
|
var hasFilter = secFilterSearch || secFilterSeverity || secFilterCategory || secFilterStatus || secFilterTimeRange;
|
|
2922
5011
|
html += '<div class="filter-bar">';
|
|
2923
|
-
html += '<input type="text" id="sec-f-search" placeholder="Search rule, finding, session..." value="' + esc(secFilterSearch) + '" onkeydown="if(event.key===\\'Enter\\')secApplyFilter()">';
|
|
5012
|
+
html += '<input type="text" id="sec-f-search" placeholder="' + esc(tr('Search rule, finding, session...', '搜索规则、命中内容、会话...')) + '" value="' + esc(secFilterSearch) + '" onkeydown="if(event.key===\\'Enter\\')secApplyFilter()">';
|
|
2924
5013
|
html += '<span class="filter-sep"></span>';
|
|
2925
5014
|
|
|
2926
5015
|
// Severity dropdown
|
|
2927
5016
|
var sevIcon = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg> ';
|
|
2928
|
-
html += secDropdown('severity', sevIcon,
|
|
5017
|
+
html += secDropdown('severity', sevIcon, secSeverityOptions(), secFilterSeverity);
|
|
2929
5018
|
|
|
2930
5019
|
// Category dropdown
|
|
2931
5020
|
var catIcon = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> ';
|
|
2932
|
-
html += secDropdown('category', catIcon,
|
|
5021
|
+
html += secDropdown('category', catIcon, secCategoryOptions(), secFilterCategory);
|
|
2933
5022
|
|
|
2934
5023
|
// Status dropdown
|
|
2935
5024
|
var staIcon = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> ';
|
|
2936
|
-
html += secDropdown('status', staIcon,
|
|
5025
|
+
html += secDropdown('status', staIcon, secStatusOptions(), secFilterStatus);
|
|
2937
5026
|
|
|
2938
5027
|
// Time range dropdown (reuse TIME_PRESETS)
|
|
2939
5028
|
html += '<div class="time-dropdown" id="sec-dd-time">';
|
|
@@ -2947,13 +5036,13 @@ async function renderSecurity() {
|
|
|
2947
5036
|
var cls = (p.key === secFilterTimeRange) ? ' active' : '';
|
|
2948
5037
|
html += '<div class="time-menu-item' + cls + '" onclick="secSelect(\\'time\\',\\'' + p.key + '\\')">';
|
|
2949
5038
|
html += '<span class="check">' + (p.key === secFilterTimeRange ? '✓' : '') + '</span>';
|
|
2950
|
-
html += esc(p
|
|
5039
|
+
html += esc(timePresetLabel(p));
|
|
2951
5040
|
html += '</div>';
|
|
2952
5041
|
});
|
|
2953
5042
|
html += '</div></div>';
|
|
2954
5043
|
|
|
2955
5044
|
if (hasFilter) {
|
|
2956
|
-
html += '<button class="btn-clear" onclick="secClearFilter()">✕
|
|
5045
|
+
html += '<button class="btn-clear" onclick="secClearFilter()">✕ ' + esc(t('clear')) + '</button>';
|
|
2957
5046
|
}
|
|
2958
5047
|
html += '</div>';
|
|
2959
5048
|
|
|
@@ -2964,44 +5053,44 @@ async function renderSecurity() {
|
|
|
2964
5053
|
});
|
|
2965
5054
|
|
|
2966
5055
|
// --- Alert list ---
|
|
2967
|
-
html += '<div class="section-title">Alerts <span class="count">' + alertData.total + (hasFilter ? ' matched' : ' total') + '</span></div>';
|
|
5056
|
+
html += '<div class="section-title">' + esc(tr('Alerts', '告警')) + ' <span class="count">' + alertData.total + (hasFilter ? (' ' + esc(t('matched'))) : (' ' + esc(t('total'))) ) + '</span></div>';
|
|
2968
5057
|
html += '<div class="alert-list">';
|
|
2969
5058
|
|
|
2970
5059
|
if (sortedAlerts.length === 0) {
|
|
2971
|
-
html += '<div class="empty"><div class="icon">✅</div><div class="text">No alerts found</div></div>';
|
|
5060
|
+
html += '<div class="empty"><div class="icon">✅</div><div class="text">' + esc(tr('No alerts found', '未发现告警')) + '</div></div>';
|
|
2972
5061
|
} else {
|
|
2973
5062
|
sortedAlerts.forEach(function(a) {
|
|
2974
5063
|
html += '<div class="alert-card sev-' + a.severity + '" id="alert-card-' + a.alert_id + '" onclick="toggleAlertDetail(event,\\'' + a.alert_id + '\\')">';
|
|
2975
5064
|
html += '<div class="alert-top">';
|
|
2976
5065
|
html += '<span class="alert-sev ' + a.severity + '">' + (SEV_ICON[a.severity]||'') + ' ' + (SEV_LABEL[a.severity]||a.severity) + '</span>';
|
|
2977
5066
|
html += '<span class="alert-rule">' + esc(a.rule_name) + '</span>';
|
|
2978
|
-
html += '<span class="alert-cat">' + esc(
|
|
2979
|
-
html += '<span class="alert-status ' + a.status + '">' + (
|
|
5067
|
+
html += '<span class="alert-cat">' + esc(secCategoryLabel(a.category)) + '</span>';
|
|
5068
|
+
html += '<span class="alert-status ' + a.status + '">' + esc(secStatusLabel(a.status)) + '</span>';
|
|
2980
5069
|
html += '</div>';
|
|
2981
5070
|
html += '<div class="alert-finding">' + esc(a.finding) + '</div>';
|
|
2982
5071
|
html += '<div class="alert-meta">';
|
|
2983
|
-
html += '<span>Rule: ' + esc(a.rule_id) + '</span>';
|
|
5072
|
+
html += '<span>' + esc(tr('Rule', '规则')) + ': ' + esc(a.rule_id) + '</span>';
|
|
2984
5073
|
var traceLink = '#/trace/' + encodeURIComponent(a.session_id) + '?action=' + encodeURIComponent(a.action_name) + '&t=' + encodeURIComponent(a.created_at || '');
|
|
2985
|
-
html += '<span>Session: <a href="' + traceLink + '">' + esc(a.session_id.substring(0,12)) + '…</a></span>';
|
|
2986
|
-
html += '<span>Action: ' + esc(a.action_name) + '</span>';
|
|
5074
|
+
html += '<span>' + esc(tr('Session', '会话')) + ': <a href="' + traceLink + '">' + esc(a.session_id.substring(0,12)) + '…</a></span>';
|
|
5075
|
+
html += '<span>' + esc(tr('Action', '动作')) + ': ' + esc(a.action_name) + '</span>';
|
|
2987
5076
|
html += '<span>' + fmtTime(a.created_at) + '</span>';
|
|
2988
5077
|
html += '</div>';
|
|
2989
5078
|
|
|
2990
5079
|
// Expandable detail area
|
|
2991
5080
|
html += '<div class="alert-detail" id="alert-detail-' + a.alert_id + '" style="display:none;margin-top:12px">';
|
|
2992
5081
|
html += '<div class="alert-detail-meta">';
|
|
2993
|
-
html += '<span><b>Alert ID:</b> ' + esc(a.alert_id) + '</span>';
|
|
2994
|
-
html += '<span><b>
|
|
2995
|
-
html += '<span><b>
|
|
5082
|
+
html += '<span><b>' + esc(tr('Alert ID', '告警 ID')) + ':</b> ' + esc(a.alert_id) + '</span>';
|
|
5083
|
+
html += '<span><b>' + esc(t('agent')) + ':</b> ' + esc(a.user_id || '-') + '</span>';
|
|
5084
|
+
html += '<span><b>' + esc(t('model')) + ':</b> ' + esc(a.model_name || '-') + '</span>';
|
|
2996
5085
|
html += '</div>';
|
|
2997
5086
|
html += '<div class="context-block">' + esc(buildAlertEvidenceText(a)) + '</div>';
|
|
2998
5087
|
html += '<div class="actions-row">';
|
|
2999
|
-
html += '<a href="' + traceLink + '" onclick="event.stopPropagation()" style="display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border-radius:var(--radius-md);border:1px solid var(--accent);background:var(--accent-subtle);color:var(--accent);font-size:12px;font-weight:600;text-decoration:none">🔍 View in Trace</a>';
|
|
5088
|
+
html += '<a href="' + traceLink + '" onclick="event.stopPropagation()" style="display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border-radius:var(--radius-md);border:1px solid var(--accent);background:var(--accent-subtle);color:var(--accent);font-size:12px;font-weight:600;text-decoration:none">🔍 ' + esc(tr('View in Trace', '在链路中查看')) + '</a>';
|
|
3000
5089
|
if (a.status === 'open') {
|
|
3001
|
-
html += '<button onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'acknowledged\\')">👁 Acknowledge</button>';
|
|
5090
|
+
html += '<button onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'acknowledged\\')">👁 ' + esc(tr('Acknowledge', '确认')) + '</button>';
|
|
3002
5091
|
}
|
|
3003
5092
|
if (a.status === 'open' || a.status === 'acknowledged') {
|
|
3004
|
-
html += '<button class="btn-resolve" onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'resolved\\')">✓ Resolve</button>';
|
|
5093
|
+
html += '<button class="btn-resolve" onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'resolved\\')">✓ ' + esc(tr('Resolve', '解决')) + '</button>';
|
|
3005
5094
|
}
|
|
3006
5095
|
html += '</div>';
|
|
3007
5096
|
html += '</div>';
|
|
@@ -3014,16 +5103,16 @@ async function renderSecurity() {
|
|
|
3014
5103
|
// Pagination
|
|
3015
5104
|
var totalPages = Math.ceil(alertData.total / 20) || 1;
|
|
3016
5105
|
html += '<div class="pagination">';
|
|
3017
|
-
html += '<button onclick="secGoPage(' + (secPage-1) + ')" ' + (secPage<=1?'disabled':'') + '>«
|
|
3018
|
-
html += '<span class="page-info">
|
|
3019
|
-
html += '<button onclick="secGoPage(' + (secPage+1) + ')" ' + (secPage>=totalPages?'disabled':'') + '>
|
|
5106
|
+
html += '<button onclick="secGoPage(' + (secPage-1) + ')" ' + (secPage<=1?'disabled':'') + '>« ' + esc(t('prev')) + '</button>';
|
|
5107
|
+
html += '<span class="page-info">' + esc(t('page')) + ' ' + secPage + ' / ' + totalPages + '</span>';
|
|
5108
|
+
html += '<button onclick="secGoPage(' + (secPage+1) + ')" ' + (secPage>=totalPages?'disabled':'') + '>' + esc(t('next')) + ' »</button>';
|
|
3020
5109
|
html += '</div>';
|
|
3021
5110
|
|
|
3022
5111
|
app.innerHTML = renderLayout('security', html);
|
|
3023
5112
|
|
|
3024
5113
|
} catch(err) {
|
|
3025
5114
|
app.innerHTML = renderLayout('security',
|
|
3026
|
-
'<div class="empty"><div class="icon">⚠️</div><div class="text">
|
|
5115
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>');
|
|
3027
5116
|
}
|
|
3028
5117
|
}
|
|
3029
5118
|
|
|
@@ -3120,14 +5209,14 @@ window.secAddCustomRegex = async function() {
|
|
|
3120
5209
|
try {
|
|
3121
5210
|
new RegExp(pattern, flags || 'i');
|
|
3122
5211
|
} catch (e) {
|
|
3123
|
-
alert('Invalid regex pattern or flags');
|
|
5212
|
+
alert(tr('Invalid regex pattern or flags', '无效的正则表达式或标记'));
|
|
3124
5213
|
return;
|
|
3125
5214
|
}
|
|
3126
5215
|
|
|
3127
5216
|
var nextRules = Array.isArray(secRuntimeCfg.customRegexRules) ? secRuntimeCfg.customRegexRules.slice() : [];
|
|
3128
5217
|
nextRules.push({
|
|
3129
5218
|
id: 'cr_' + Date.now() + '_' + Math.floor(Math.random() * 10000),
|
|
3130
|
-
name: name || 'Custom Regex',
|
|
5219
|
+
name: name || tr('Custom Regex', '自定义规则'),
|
|
3131
5220
|
pattern: pattern,
|
|
3132
5221
|
flags: flags || 'i',
|
|
3133
5222
|
severity: severity,
|
|
@@ -3215,17 +5304,10 @@ window.updateAlertSt = async function(alertId, newStatus) {
|
|
|
3215
5304
|
window.__alertCount -= 1;
|
|
3216
5305
|
}
|
|
3217
5306
|
|
|
3218
|
-
var
|
|
3219
|
-
|
|
3220
|
-
var
|
|
3221
|
-
if (!
|
|
3222
|
-
if (labelNode.textContent.trim().toLowerCase() === 'open') {
|
|
3223
|
-
var valueNode = labelNode.parentElement ? labelNode.parentElement.querySelector('.stat-value') : null;
|
|
3224
|
-
if (valueNode) {
|
|
3225
|
-
var cur = parseInt((valueNode.textContent || '0').trim(), 10);
|
|
3226
|
-
if (!isNaN(cur) && cur > 0) valueNode.textContent = String(cur - 1);
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
5307
|
+
var openNode = document.getElementById('sec-open-count');
|
|
5308
|
+
if (openNode) {
|
|
5309
|
+
var cur = parseInt((openNode.textContent || '0').trim(), 10);
|
|
5310
|
+
if (!isNaN(cur) && cur > 0) openNode.textContent = String(cur - 1);
|
|
3229
5311
|
}
|
|
3230
5312
|
} catch(e) {
|
|
3231
5313
|
console.error('Failed to update alert:', e);
|
|
@@ -3322,6 +5404,7 @@ function observationsToActions(sessionId, observations) {
|
|
|
3322
5404
|
id: obs.observationId,
|
|
3323
5405
|
__observation_id: obs.observationId,
|
|
3324
5406
|
__parent_observation_id: obs.parentObservationId || null,
|
|
5407
|
+
parent_run_id: obs.parentRunId || null,
|
|
3325
5408
|
__start_at: startAt,
|
|
3326
5409
|
__end_at: endAt,
|
|
3327
5410
|
session_id: sessionId,
|
|
@@ -3379,6 +5462,38 @@ function parseActionFieldJson(raw) {
|
|
|
3379
5462
|
try { return JSON.parse(raw); } catch (_) { return null; }
|
|
3380
5463
|
}
|
|
3381
5464
|
|
|
5465
|
+
function filterInvalidTranscriptThinking(actions) {
|
|
5466
|
+
var list = ensureArray(actions);
|
|
5467
|
+
if (!list.length) return list;
|
|
5468
|
+
var runToolCallIds = {};
|
|
5469
|
+
list.forEach(function(a) {
|
|
5470
|
+
if (!a) return;
|
|
5471
|
+
var runId = getActionRunId(a) || '';
|
|
5472
|
+
if (!runId) return;
|
|
5473
|
+
if (a.action_type !== 'tool_call' && a.action_type !== 'tool_update') return;
|
|
5474
|
+
var tcid = getToolCallId(a) || '';
|
|
5475
|
+
if (!tcid) return;
|
|
5476
|
+
if (!runToolCallIds[runId]) runToolCallIds[runId] = {};
|
|
5477
|
+
runToolCallIds[runId][tcid] = true;
|
|
5478
|
+
});
|
|
5479
|
+
return list.filter(function(a) {
|
|
5480
|
+
if (!a || a.action_type !== 'thinking') return true;
|
|
5481
|
+
var input = parseActionFieldJson(a.input_params);
|
|
5482
|
+
if (!input || typeof input !== 'object') return true;
|
|
5483
|
+
if (String(input.source || '') !== 'transcript_message') return true;
|
|
5484
|
+
if (!input.synthetic) return true;
|
|
5485
|
+
var toolCallIds = Array.isArray(input.toolCallIds) ? input.toolCallIds.map(function(v){ return String(v || ''); }).filter(Boolean) : [];
|
|
5486
|
+
if (!toolCallIds.length) return true;
|
|
5487
|
+
var runId = String(input.runId || getActionRunId(a) || '');
|
|
5488
|
+
if (!runId) return true;
|
|
5489
|
+
var known = runToolCallIds[runId] || {};
|
|
5490
|
+
for (var i = 0; i < toolCallIds.length; i++) {
|
|
5491
|
+
if (known[toolCallIds[i]]) return true;
|
|
5492
|
+
}
|
|
5493
|
+
return false;
|
|
5494
|
+
});
|
|
5495
|
+
}
|
|
5496
|
+
|
|
3382
5497
|
async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts) {
|
|
3383
5498
|
opts = opts || {};
|
|
3384
5499
|
selectedActionIdx = -1;
|
|
@@ -3389,7 +5504,7 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3389
5504
|
var shouldUseCache = !!opts.useCache && !!cachedTrace;
|
|
3390
5505
|
if (!shouldUseCache) {
|
|
3391
5506
|
app.innerHTML = renderLayout('trace',
|
|
3392
|
-
'<div class="trace-header"><div class="loading">
|
|
5507
|
+
'<div class="trace-header"><div class="loading">' + esc(t('trace_detail_loading')) + '</div></div>');
|
|
3393
5508
|
}
|
|
3394
5509
|
|
|
3395
5510
|
try {
|
|
@@ -3416,12 +5531,28 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3416
5531
|
var sessionSnapshots = ensureArray(actions).filter(function(a) {
|
|
3417
5532
|
return a && a.action_type === 'session_snapshot';
|
|
3418
5533
|
});
|
|
5534
|
+
var agentEndActions = ensureArray(actions).filter(function(a) {
|
|
5535
|
+
return a && a.action_type === 'agent_end';
|
|
5536
|
+
});
|
|
5537
|
+
var latestAgentEnd = agentEndActions.length ? agentEndActions[agentEndActions.length - 1] : null;
|
|
5538
|
+
var traceRunState = 'running';
|
|
5539
|
+
if (latestAgentEnd) {
|
|
5540
|
+
var endPayload = parseActionFieldJson(latestAgentEnd.output_result) || {};
|
|
5541
|
+
traceRunState = endPayload && endPayload.success === false ? 'failed' : 'completed';
|
|
5542
|
+
}
|
|
3419
5543
|
var timelineActions = ensureArray(actions).filter(function(a) {
|
|
3420
|
-
return a && a.action_type !== 'session_snapshot';
|
|
5544
|
+
return a && a.action_type !== 'session_snapshot' && a.action_type !== 'agent_end';
|
|
3421
5545
|
});
|
|
5546
|
+
timelineActions = filterInvalidTranscriptThinking(timelineActions);
|
|
3422
5547
|
var latestSessionSnapshot = sessionSnapshots.length
|
|
3423
5548
|
? parseActionFieldJson(sessionSnapshots[sessionSnapshots.length - 1].output_result)
|
|
3424
5549
|
: null;
|
|
5550
|
+
var traceSubagents = [];
|
|
5551
|
+
try {
|
|
5552
|
+
traceSubagents = buildTraceSubagentSummary(timelineActions);
|
|
5553
|
+
} catch (_) {
|
|
5554
|
+
traceSubagents = [];
|
|
5555
|
+
}
|
|
3425
5556
|
|
|
3426
5557
|
if (timelineActions.length === 0) {
|
|
3427
5558
|
app.innerHTML = renderLayout('trace',
|
|
@@ -3434,10 +5565,20 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3434
5565
|
|
|
3435
5566
|
// Build spans with timing
|
|
3436
5567
|
var spans = timelineActions.map(function(a, idx) {
|
|
5568
|
+
var hasExplicitStart = !!a.__start_at;
|
|
5569
|
+
var hasExplicitEnd = !!a.__end_at;
|
|
3437
5570
|
var startMs = new Date(a.__start_at || a.created_at).getTime();
|
|
3438
5571
|
var endMs = new Date(a.__end_at || a.created_at).getTime();
|
|
3439
5572
|
if (!Number.isFinite(startMs)) startMs = new Date(a.created_at).getTime();
|
|
3440
5573
|
if (!Number.isFinite(endMs)) endMs = new Date(a.created_at).getTime();
|
|
5574
|
+
// Legacy/fallback rows (from observation_actions) only carry created_at + duration_ms.
|
|
5575
|
+
// In those cases created_at is effectively end-time; backfill start-time for timeline.
|
|
5576
|
+
if (!hasExplicitStart && !hasExplicitEnd) {
|
|
5577
|
+
var durMs = Number(a.duration_ms);
|
|
5578
|
+
if (Number.isFinite(durMs) && durMs > 0) {
|
|
5579
|
+
startMs = endMs - durMs;
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
3441
5582
|
if (endMs < startMs) endMs = startMs;
|
|
3442
5583
|
if ((a.duration_ms == null || !Number.isFinite(Number(a.duration_ms))) && endMs > startMs) {
|
|
3443
5584
|
a.duration_ms = endMs - startMs;
|
|
@@ -3475,194 +5616,28 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3475
5616
|
return lv;
|
|
3476
5617
|
}
|
|
3477
5618
|
|
|
3478
|
-
function findOwnerLlmStartMs(thinkingSpan, allSpans) {
|
|
3479
|
-
var owner = null;
|
|
3480
|
-
for (var i = 0; i < allSpans.length; i++) {
|
|
3481
|
-
var span = allSpans[i];
|
|
3482
|
-
if (span.action.action_type !== 'message') continue;
|
|
3483
|
-
if (!span.action.action_name || span.action.action_name.indexOf('llm_call:') !== 0) continue;
|
|
3484
|
-
if (span.startMs <= thinkingSpan.endMs && span.endMs >= thinkingSpan.endMs) {
|
|
3485
|
-
if (!owner || span.endMs < owner.endMs) owner = span;
|
|
3486
|
-
}
|
|
3487
|
-
}
|
|
3488
|
-
return owner ? owner.startMs : null;
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
// For thinking rows without explicit duration, infer a visible span from the
|
|
3492
|
-
// previous action end to current thinking timestamp.
|
|
3493
|
-
var spansByEndTime = ensureArray(spans).slice().sort(function(a, b) {
|
|
3494
|
-
var diff = a.endMs - b.endMs;
|
|
3495
|
-
if (diff !== 0) return diff;
|
|
3496
|
-
return a.idx - b.idx;
|
|
3497
|
-
});
|
|
3498
|
-
var prevEndMs = null;
|
|
3499
|
-
spansByEndTime.forEach(function(span) {
|
|
3500
|
-
if (
|
|
3501
|
-
span.action.action_type === 'thinking' &&
|
|
3502
|
-
(span.action.duration_ms == null || span.action.duration_ms <= 0) &&
|
|
3503
|
-
prevEndMs != null &&
|
|
3504
|
-
span.endMs > prevEndMs
|
|
3505
|
-
) {
|
|
3506
|
-
var ownerLlmStartMs = findOwnerLlmStartMs(span, spans);
|
|
3507
|
-
var inferredStartMs = prevEndMs;
|
|
3508
|
-
if (ownerLlmStartMs != null) {
|
|
3509
|
-
inferredStartMs = Math.max(inferredStartMs, ownerLlmStartMs);
|
|
3510
|
-
}
|
|
3511
|
-
span.startMs = inferredStartMs;
|
|
3512
|
-
span.displayDurationMs = span.endMs - span.startMs;
|
|
3513
|
-
}
|
|
3514
|
-
prevEndMs = Math.max(prevEndMs == null ? span.endMs : prevEndMs, span.endMs);
|
|
3515
|
-
});
|
|
3516
|
-
|
|
3517
5619
|
// Determine time range (recomputed again after tree normalization)
|
|
3518
5620
|
var sessionStart = Math.min.apply(null, spans.map(function(s){ return s.startMs; }));
|
|
3519
5621
|
var sessionEnd = Math.max.apply(null, spans.map(function(s){ return s.endMs; }));
|
|
3520
5622
|
var totalDur = sessionEnd - sessionStart || 1;
|
|
3521
5623
|
|
|
3522
5624
|
// ---- Sorting + nesting ----
|
|
3523
|
-
//
|
|
3524
|
-
//
|
|
3525
|
-
function typePriority(actionType) {
|
|
3526
|
-
if (actionType === 'agent_end') return 0;
|
|
3527
|
-
if (actionType === 'message') return 1;
|
|
3528
|
-
if (actionType === 'model_resolve') return 2;
|
|
3529
|
-
if (actionType === 'prompt_build') return 3;
|
|
3530
|
-
if (actionType === 'tool_call') return 2;
|
|
3531
|
-
if (actionType === 'assistant_stream') return 4;
|
|
3532
|
-
if (actionType === 'thinking') return 5;
|
|
3533
|
-
if (actionType === 'tool_update') return 6;
|
|
3534
|
-
return 10;
|
|
3535
|
-
}
|
|
3536
|
-
function canBecomeParent(actionType) {
|
|
3537
|
-
return actionType === 'agent_end' || actionType === 'message' || actionType === 'tool_call';
|
|
3538
|
-
}
|
|
3539
|
-
// Sort by startMs asc; when startMs is close (within 2s), prioritize by semantic priority; then by duration desc
|
|
5625
|
+
// Strict mode: frontend does not infer or rewrite timeline semantics.
|
|
5626
|
+
// Keep only chronological ordering from backend timestamps.
|
|
3540
5627
|
ensureArray(spans).sort(function(a, b) {
|
|
3541
5628
|
var startDiff = a.startMs - b.startMs;
|
|
3542
5629
|
if (startDiff !== 0) return startDiff;
|
|
3543
|
-
var priA = typePriority(a.action.action_type);
|
|
3544
|
-
var priB = typePriority(b.action.action_type);
|
|
3545
|
-
if (priA !== priB) return priA - priB;
|
|
3546
5630
|
var endDiff = a.endMs - b.endMs;
|
|
3547
5631
|
if (endDiff !== 0) return endDiff;
|
|
3548
5632
|
return a.idx - b.idx;
|
|
3549
5633
|
});
|
|
3550
5634
|
|
|
3551
|
-
var toolParentsById = {};
|
|
3552
|
-
spans.forEach(function(span) {
|
|
3553
|
-
if (span.action.action_type === 'tool_call') {
|
|
3554
|
-
var toolCallId = getToolCallId(span.action);
|
|
3555
|
-
if (!toolCallId) return;
|
|
3556
|
-
if (!toolParentsById[toolCallId]) toolParentsById[toolCallId] = [];
|
|
3557
|
-
toolParentsById[toolCallId].push(span);
|
|
3558
|
-
}
|
|
3559
|
-
});
|
|
3560
|
-
Object.keys(toolParentsById).forEach(function(id) {
|
|
3561
|
-
toolParentsById[id].sort(function(a, b) {
|
|
3562
|
-
var diff = a.startMs - b.startMs;
|
|
3563
|
-
if (diff !== 0) return diff;
|
|
3564
|
-
return a.idx - b.idx;
|
|
3565
|
-
});
|
|
3566
|
-
});
|
|
3567
|
-
|
|
3568
|
-
function resolveExactToolParent(span, toolCallId) {
|
|
3569
|
-
var candidates = toolParentsById[toolCallId];
|
|
3570
|
-
if (!candidates || candidates.length === 0) return null;
|
|
3571
|
-
var containing = null;
|
|
3572
|
-
for (var i = 0; i < candidates.length; i++) {
|
|
3573
|
-
var parent = candidates[i];
|
|
3574
|
-
if (parent.startMs <= span.endMs + 50 && parent.endMs >= span.startMs - 1000) {
|
|
3575
|
-
containing = parent;
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
if (containing) return containing;
|
|
3579
|
-
|
|
3580
|
-
var nearest = null;
|
|
3581
|
-
var nearestGap = Infinity;
|
|
3582
|
-
for (var j = 0; j < candidates.length; j++) {
|
|
3583
|
-
var p = candidates[j];
|
|
3584
|
-
var gap = Math.abs(p.endMs - span.endMs);
|
|
3585
|
-
if (gap < nearestGap) {
|
|
3586
|
-
nearest = p;
|
|
3587
|
-
nearestGap = gap;
|
|
3588
|
-
}
|
|
3589
|
-
}
|
|
3590
|
-
return nearest;
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
5635
|
var flatSpans = [];
|
|
3594
5636
|
if (hasTreeParents) {
|
|
3595
|
-
function inferDisplayParentId(span) {
|
|
3596
|
-
var pid = span.action && span.action.__parent_observation_id ? String(span.action.__parent_observation_id) : '';
|
|
3597
|
-
if (pid && spansByObsId[pid]) return pid;
|
|
3598
|
-
if (!span.action) return '';
|
|
3599
|
-
if (span.action.action_type !== 'tool_update') return pid;
|
|
3600
|
-
|
|
3601
|
-
var runId = getActionRunId(span.action) || '';
|
|
3602
|
-
var toolCallId = getToolCallId(span.action) || '';
|
|
3603
|
-
var toolName = '';
|
|
3604
|
-
var actionName = String(span.action.action_name || '');
|
|
3605
|
-
var cidx = actionName.indexOf(':');
|
|
3606
|
-
if (cidx >= 0 && cidx < actionName.length - 1) {
|
|
3607
|
-
toolName = actionName.slice(cidx + 1);
|
|
3608
|
-
}
|
|
3609
|
-
|
|
3610
|
-
// Prefer exact tool_call parent by (runId, toolCallId)
|
|
3611
|
-
if (runId && toolCallId) {
|
|
3612
|
-
for (var i = 0; i < spans.length; i++) {
|
|
3613
|
-
var p = spans[i];
|
|
3614
|
-
if (!p || !p.action) continue;
|
|
3615
|
-
if (p.action.action_type !== 'tool_call') continue;
|
|
3616
|
-
if ((getActionRunId(p.action) || '') !== runId) continue;
|
|
3617
|
-
if ((getToolCallId(p.action) || '') !== toolCallId) continue;
|
|
3618
|
-
if (p.action.__observation_id) return String(p.action.__observation_id);
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
|
|
3622
|
-
// Fallback: nearest tool_call with same name (and same runId when available)
|
|
3623
|
-
var best = null;
|
|
3624
|
-
var bestScore = Infinity;
|
|
3625
|
-
for (var k = 0; k < spans.length; k++) {
|
|
3626
|
-
var tc = spans[k];
|
|
3627
|
-
if (!tc || !tc.action || tc.action.action_type !== 'tool_call') continue;
|
|
3628
|
-
if (runId && (getActionRunId(tc.action) || '') !== runId) continue;
|
|
3629
|
-
if (toolName) {
|
|
3630
|
-
var tcName = String(tc.action.action_name || '');
|
|
3631
|
-
var tci = tcName.indexOf(':');
|
|
3632
|
-
var tcTool = tci >= 0 && tci < tcName.length - 1 ? tcName.slice(tci + 1) : '';
|
|
3633
|
-
if (tcTool && tcTool !== toolName) continue;
|
|
3634
|
-
}
|
|
3635
|
-
var score = Math.abs(span.startMs - tc.endMs);
|
|
3636
|
-
if (score < bestScore) {
|
|
3637
|
-
bestScore = score;
|
|
3638
|
-
best = tc;
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
|
-
if (best && best.action && best.action.__observation_id) {
|
|
3642
|
-
return String(best.action.__observation_id);
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
// Fallback to run-level llm_call parent
|
|
3646
|
-
if (runId) {
|
|
3647
|
-
for (var j = 0; j < spans.length; j++) {
|
|
3648
|
-
var lp = spans[j];
|
|
3649
|
-
if (!lp || !lp.action) continue;
|
|
3650
|
-
if (lp.action.action_type !== 'message') continue;
|
|
3651
|
-
var an = String(lp.action.action_name || '');
|
|
3652
|
-
if (an.indexOf('llm_call:') !== 0) continue;
|
|
3653
|
-
if ((getActionRunId(lp.action) || '') !== runId) continue;
|
|
3654
|
-
if (lp.action.__observation_id) return String(lp.action.__observation_id);
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
|
|
3658
|
-
return pid;
|
|
3659
|
-
}
|
|
3660
|
-
|
|
3661
5637
|
var childrenByParent = {};
|
|
3662
5638
|
var roots = [];
|
|
3663
5639
|
spans.forEach(function(span) {
|
|
3664
|
-
var
|
|
3665
|
-
var pid = inferDisplayParentId(span);
|
|
5640
|
+
var pid = span.action && span.action.__parent_observation_id ? String(span.action.__parent_observation_id) : '';
|
|
3666
5641
|
if (!pid || !spansByObsId[pid]) {
|
|
3667
5642
|
roots.push(span);
|
|
3668
5643
|
return;
|
|
@@ -3687,113 +5662,17 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3687
5662
|
kids.forEach(function(child) { dfs(child, depth + 1); });
|
|
3688
5663
|
}
|
|
3689
5664
|
roots.forEach(function(root) { dfs(root, 0); });
|
|
3690
|
-
// Normalize timeline in tree mode:
|
|
3691
|
-
// 1) parent covers children window
|
|
3692
|
-
// 2) for llm children, thinking ends before assistant_stream starts (display order)
|
|
3693
|
-
flatSpans.forEach(function(span) {
|
|
3694
|
-
var oid = span.action && span.action.__observation_id ? String(span.action.__observation_id) : '';
|
|
3695
|
-
if (!oid) return;
|
|
3696
|
-
var kids = childrenByParent[oid] || [];
|
|
3697
|
-
if (!kids.length) return;
|
|
3698
|
-
var minStart = Math.min.apply(null, kids.map(function(k){ return k.startMs; }));
|
|
3699
|
-
var maxEnd = Math.max.apply(null, kids.map(function(k){ return k.endMs; }));
|
|
3700
|
-
if (Number.isFinite(minStart)) span.startMs = Math.min(span.startMs, minStart);
|
|
3701
|
-
if (Number.isFinite(maxEnd)) span.endMs = Math.max(span.endMs, maxEnd);
|
|
3702
|
-
if (span.endMs < span.startMs) span.endMs = span.startMs;
|
|
3703
|
-
|
|
3704
|
-
if (span.action && span.action.action_type === 'message' && String(span.action.action_name || '').indexOf('llm_call:') === 0) {
|
|
3705
|
-
var thinkingChildren = [];
|
|
3706
|
-
var assistantChild = null;
|
|
3707
|
-
var maxToolEnd = null;
|
|
3708
|
-
kids.forEach(function(k) {
|
|
3709
|
-
if (k.action && k.action.action_type === 'thinking') thinkingChildren.push(k);
|
|
3710
|
-
if (k.action && k.action.action_type === 'assistant_stream' && !assistantChild) assistantChild = k;
|
|
3711
|
-
if (k.action && (k.action.action_type === 'tool_call' || k.action.action_type === 'tool_update')) {
|
|
3712
|
-
maxToolEnd = maxToolEnd == null ? k.endMs : Math.max(maxToolEnd, k.endMs);
|
|
3713
|
-
}
|
|
3714
|
-
});
|
|
3715
|
-
var thinkingChild = null;
|
|
3716
|
-
if (thinkingChildren.length > 0) {
|
|
3717
|
-
if (assistantChild) {
|
|
3718
|
-
// Use the latest thinking span before assistant starts.
|
|
3719
|
-
// Earlier thinking fragments should keep their own timestamps.
|
|
3720
|
-
for (var ti = 0; ti < thinkingChildren.length; ti++) {
|
|
3721
|
-
var candidate = thinkingChildren[ti];
|
|
3722
|
-
if (candidate.startMs <= assistantChild.startMs + 1) {
|
|
3723
|
-
thinkingChild = candidate;
|
|
3724
|
-
}
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
if (!thinkingChild) {
|
|
3728
|
-
thinkingChild = thinkingChildren[thinkingChildren.length - 1];
|
|
3729
|
-
}
|
|
3730
|
-
}
|
|
3731
|
-
if (thinkingChild && assistantChild && assistantChild.startMs < thinkingChild.endMs) {
|
|
3732
|
-
thinkingChild.endMs = assistantChild.startMs;
|
|
3733
|
-
if (thinkingChild.endMs < thinkingChild.startMs) {
|
|
3734
|
-
thinkingChild.startMs = thinkingChild.endMs;
|
|
3735
|
-
}
|
|
3736
|
-
if (thinkingChild.action) {
|
|
3737
|
-
thinkingChild.action.duration_ms = Math.max(0, thinkingChild.endMs - thinkingChild.startMs);
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
// If tools happened after thinking but before assistant, keep thinking continuous
|
|
3741
|
-
// so timeline does not show an empty gap.
|
|
3742
|
-
if (
|
|
3743
|
-
thinkingChild &&
|
|
3744
|
-
assistantChild &&
|
|
3745
|
-
maxToolEnd != null &&
|
|
3746
|
-
maxToolEnd >= thinkingChild.endMs - 5 &&
|
|
3747
|
-
assistantChild.startMs > thinkingChild.endMs
|
|
3748
|
-
) {
|
|
3749
|
-
thinkingChild.endMs = assistantChild.startMs;
|
|
3750
|
-
if (thinkingChild.endMs < thinkingChild.startMs) {
|
|
3751
|
-
thinkingChild.startMs = thinkingChild.endMs;
|
|
3752
|
-
}
|
|
3753
|
-
if (thinkingChild.action) {
|
|
3754
|
-
thinkingChild.action.duration_ms = Math.max(0, thinkingChild.endMs - thinkingChild.startMs);
|
|
3755
|
-
}
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
});
|
|
3759
5665
|
// orphan safety
|
|
3760
5666
|
spans.forEach(function(span) {
|
|
3761
5667
|
if (flatSpans.indexOf(span) >= 0) return;
|
|
3762
|
-
span.level =
|
|
5668
|
+
span.level = 0;
|
|
3763
5669
|
flatSpans.push(span);
|
|
3764
5670
|
});
|
|
3765
5671
|
} else {
|
|
3766
|
-
//
|
|
3767
|
-
var parentStack = []; // { startMs, endMs, level, actionType }
|
|
5672
|
+
// Legacy rows without structured parent links: keep strict chronological list.
|
|
3768
5673
|
spans.forEach(function(span) {
|
|
3769
|
-
|
|
3770
|
-
var spanToolCallId = getToolCallId(span.action);
|
|
3771
|
-
if (span.action.action_type !== 'tool_call' && spanToolCallId) {
|
|
3772
|
-
exactToolParent = resolveExactToolParent(span, spanToolCallId);
|
|
3773
|
-
}
|
|
3774
|
-
while (parentStack.length > 0 &&
|
|
3775
|
-
parentStack[parentStack.length - 1].endMs < span.startMs - 1000) {
|
|
3776
|
-
parentStack.pop();
|
|
3777
|
-
}
|
|
3778
|
-
if (exactToolParent) {
|
|
3779
|
-
span.level = exactToolParent.level + 1;
|
|
3780
|
-
} else if (span.action.action_type === 'tool_call') {
|
|
3781
|
-
span.level = parentStack.filter(function(p) { return p.actionType !== 'tool_call'; }).length;
|
|
3782
|
-
} else if (span.action.action_type === 'thinking') {
|
|
3783
|
-
span.level = parentStack.filter(function(p) { return p.actionType !== 'tool_call'; }).length;
|
|
3784
|
-
} else {
|
|
3785
|
-
span.level = parentStack.length;
|
|
3786
|
-
}
|
|
5674
|
+
span.level = 0;
|
|
3787
5675
|
flatSpans.push(span);
|
|
3788
|
-
var dur = span.endMs - span.startMs;
|
|
3789
|
-
if (dur > 100 && canBecomeParent(span.action.action_type)) {
|
|
3790
|
-
parentStack.push({
|
|
3791
|
-
startMs: span.startMs,
|
|
3792
|
-
endMs: span.endMs,
|
|
3793
|
-
level: span.level,
|
|
3794
|
-
actionType: span.action.action_type
|
|
3795
|
-
});
|
|
3796
|
-
}
|
|
3797
5676
|
});
|
|
3798
5677
|
}
|
|
3799
5678
|
|
|
@@ -3831,9 +5710,50 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3831
5710
|
});
|
|
3832
5711
|
|
|
3833
5712
|
if (runGroups.length > 0) {
|
|
3834
|
-
|
|
3835
|
-
.map(function(g) { return { runId: g.runId, rootSpan: null, items: g.items }; })
|
|
5713
|
+
var normalized = runGroups
|
|
5714
|
+
.map(function(g) { return { runId: g.runId, parentRunId: '', rootSpan: null, items: g.items.slice(), startMs: g.startMs, endMs: g.endMs, mergedSubagents: [] }; })
|
|
3836
5715
|
.filter(function(g) { return g.items && g.items.length; });
|
|
5716
|
+
|
|
5717
|
+
// Strict merge rule: only merge when parent_run_id is explicitly present in data.
|
|
5718
|
+
normalized.forEach(function(g) {
|
|
5719
|
+
ensureArray(g.items).forEach(function(span) {
|
|
5720
|
+
if (!span || !span.action || g.parentRunId) return;
|
|
5721
|
+
var explicitParent = getActionParentRunId(span.action);
|
|
5722
|
+
if (explicitParent) g.parentRunId = explicitParent;
|
|
5723
|
+
});
|
|
5724
|
+
});
|
|
5725
|
+
|
|
5726
|
+
var byRunId = {};
|
|
5727
|
+
normalized.forEach(function(g) { byRunId[g.runId] = g; });
|
|
5728
|
+
var removeRun = {};
|
|
5729
|
+
normalized.forEach(function(g) {
|
|
5730
|
+
if (!g || !g.runId) return;
|
|
5731
|
+
var parentRunId = String(g.parentRunId || '');
|
|
5732
|
+
if (!parentRunId || parentRunId === g.runId) return;
|
|
5733
|
+
var parent = byRunId[parentRunId];
|
|
5734
|
+
if (!parent) return;
|
|
5735
|
+
|
|
5736
|
+
var announceInfo = parseAnnounceRunInfo(g.runId);
|
|
5737
|
+
var childRunId = announceInfo ? announceInfo.childRunId : g.runId;
|
|
5738
|
+
var childSessionKey = announceInfo ? announceInfo.childSessionKey : '';
|
|
5739
|
+
|
|
5740
|
+
parent.items = ensureArray(parent.items).concat(ensureArray(g.items));
|
|
5741
|
+
parent.startMs = Math.min(Number(parent.startMs || 0), Number(g.startMs || parent.startMs || 0));
|
|
5742
|
+
parent.endMs = Math.max(Number(parent.endMs || 0), Number(g.endMs || parent.endMs || 0));
|
|
5743
|
+
parent.mergedSubagents = ensureArray(parent.mergedSubagents);
|
|
5744
|
+
var exists = parent.mergedSubagents.some(function(x) { return x && x.childRunId === childRunId; });
|
|
5745
|
+
if (!exists) {
|
|
5746
|
+
parent.mergedSubagents.push({
|
|
5747
|
+
childRunId: childRunId,
|
|
5748
|
+
childSessionKey: childSessionKey,
|
|
5749
|
+
});
|
|
5750
|
+
}
|
|
5751
|
+
removeRun[g.runId] = true;
|
|
5752
|
+
});
|
|
5753
|
+
|
|
5754
|
+
return normalized
|
|
5755
|
+
.filter(function(g) { return !removeRun[g.runId]; })
|
|
5756
|
+
.map(function(g) { return { runId: g.runId, rootSpan: null, items: g.items, mergedSubagents: g.mergedSubagents || [] }; });
|
|
3837
5757
|
}
|
|
3838
5758
|
|
|
3839
5759
|
var rootSpans = spansInOrder.filter(function(span) {
|
|
@@ -3930,27 +5850,64 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3930
5850
|
// Trace header
|
|
3931
5851
|
html += '<div class="trace-header">';
|
|
3932
5852
|
html += '<div class="trace-header-top">';
|
|
3933
|
-
html += '<a href="#/traces" class="trace-back">←
|
|
5853
|
+
html += '<a href="#/traces" class="trace-back">← ' + esc(t('back_to_traces')) + '</a>';
|
|
5854
|
+
html += '<div class="trace-header-actions">';
|
|
5855
|
+
html += '<span class="trace-run-state ' + traceRunState + '">' + (traceRunState === 'completed' ? t('completed') : (traceRunState === 'failed' ? t('failed') : t('running'))) + '</span>';
|
|
5856
|
+
html += '</div>';
|
|
3934
5857
|
html += '</div>';
|
|
3935
5858
|
html += '<div class="trace-title">Session: ' + esc(sessionId) + '</div>';
|
|
3936
5859
|
html += '<div class="trace-meta">';
|
|
3937
5860
|
html += '<span class="trace-meta-item"><b>Agent:</b> ' + esc(userId) + '</span>';
|
|
3938
5861
|
html += '<span class="trace-meta-item"><b>Model:</b> ' + esc(modelName) + '</span>';
|
|
3939
|
-
html += '<span class="trace-meta-item"><b>
|
|
3940
|
-
html += '<span class="trace-meta-item"><b>
|
|
5862
|
+
html += '<span class="trace-meta-item"><b>' + esc(t('duration')) + ':</b> ' + fmtDur(totalDur) + '</span>';
|
|
5863
|
+
html += '<span class="trace-meta-item"><b>' + esc(t('actions')) + ':</b> ' + timelineActions.length + '</span>';
|
|
3941
5864
|
if (sessionSnapshots.length > 0) {
|
|
3942
|
-
html += '<span class="trace-meta-item"><b>
|
|
5865
|
+
html += '<span class="trace-meta-item"><b>' + esc(t('snapshots')) + ':</b> ' + sessionSnapshots.length + '</span>';
|
|
3943
5866
|
}
|
|
3944
|
-
html += '<span class="trace-meta-item"><b>
|
|
5867
|
+
html += '<span class="trace-meta-item"><b>' + esc(t('time')) + ':</b> ' + fmtTime(new Date(firstAction.startMs).toISOString()) + ' → ' + fmtTime(new Date(lastAction.endMs).toISOString()) + '</span>';
|
|
3945
5868
|
html += '</div>';
|
|
5869
|
+
if (traceSubagents.length > 0) {
|
|
5870
|
+
html += '<div class="trace-subagents">';
|
|
5871
|
+
traceSubagents.forEach(function(sa) {
|
|
5872
|
+
var statusCls = sa.status === 'done' ? 'done' : (sa.status === 'fail' ? 'fail' : 'running');
|
|
5873
|
+
var statusText = sa.status === 'done' ? 'completed' : (sa.status === 'fail' ? 'failed' : 'running');
|
|
5874
|
+
var title = sa.label || sa.task || 'subagent task';
|
|
5875
|
+
var keyText = shortSessionKey(sa.childSessionKey);
|
|
5876
|
+
html += '<div class="trace-subagent-item">';
|
|
5877
|
+
html += '<div class="trace-subagent-row">';
|
|
5878
|
+
html += '<span class="trace-subagent-name">' + esc(title) + '</span>';
|
|
5879
|
+
html += '<span class="trace-subagent-status ' + statusCls + '">' + statusText + '</span>';
|
|
5880
|
+
html += '</div>';
|
|
5881
|
+
html += '<div class="trace-subagent-meta">';
|
|
5882
|
+
if (sa.runtime) {
|
|
5883
|
+
html += '<div><b>Runtime:</b> ' + esc(sa.runtime) + '</div>';
|
|
5884
|
+
}
|
|
5885
|
+
if (sa.spawnedAt) {
|
|
5886
|
+
html += '<div><b>Spawned:</b> ' + fmtTime(sa.spawnedAt) + '</div>';
|
|
5887
|
+
}
|
|
5888
|
+
if (sa.completedAt) {
|
|
5889
|
+
html += '<div><b>Completed:</b> ' + fmtTime(sa.completedAt) + '</div>';
|
|
5890
|
+
}
|
|
5891
|
+
html += '</div>';
|
|
5892
|
+
html += '<div class="trace-subagent-link">';
|
|
5893
|
+
if (sa.childSessionId) {
|
|
5894
|
+
html += '<a href="#/trace/' + encodeURIComponent(sa.childSessionId) + '">' + esc(keyText) + '</a>';
|
|
5895
|
+
} else {
|
|
5896
|
+
html += esc(keyText);
|
|
5897
|
+
}
|
|
5898
|
+
html += '</div>';
|
|
5899
|
+
html += '</div>';
|
|
5900
|
+
});
|
|
5901
|
+
html += '</div>';
|
|
5902
|
+
}
|
|
3946
5903
|
html += '<div class="filter-bar trace-search-bar">';
|
|
3947
|
-
html += '<input type="text" id="trace-search-input" placeholder="
|
|
5904
|
+
html += '<input type="text" id="trace-search-input" placeholder="' + esc(t('search_this_session')) + '" value="' + esc(traceSearchQuery) + '" onkeydown="if(event.key===\\'Enter\\')applyTraceSearch()">';
|
|
3948
5905
|
html += '<div class="trace-search-nav">';
|
|
3949
|
-
html += '<button class="icon-refresh-btn" onclick="refreshCurrentTrace()" title="
|
|
3950
|
-
html += '<button onclick="applyTraceSearch()">
|
|
3951
|
-
html += '<button onclick="traceSearchStep(-1)"' + (matchedFlatIdxs.length ? '' : ' disabled') + '>
|
|
3952
|
-
html += '<button onclick="traceSearchStep(1)"' + (matchedFlatIdxs.length ? '' : ' disabled') + '>
|
|
3953
|
-
html += '<button class="btn-clear" onclick="clearTraceSearch()">
|
|
5906
|
+
html += '<button class="icon-refresh-btn" onclick="refreshCurrentTrace()" title="' + esc(t('refresh_current_trace')) + '">' + ICON_REFRESH + '</button>';
|
|
5907
|
+
html += '<button onclick="applyTraceSearch()">' + esc(t('search')) + '</button>';
|
|
5908
|
+
html += '<button onclick="traceSearchStep(-1)"' + (matchedFlatIdxs.length ? '' : ' disabled') + '>' + esc(t('prev')) + '</button>';
|
|
5909
|
+
html += '<button onclick="traceSearchStep(1)"' + (matchedFlatIdxs.length ? '' : ' disabled') + '>' + esc(t('next')) + '</button>';
|
|
5910
|
+
html += '<button class="btn-clear" onclick="clearTraceSearch()">' + esc(t('clear')) + '</button>';
|
|
3954
5911
|
html += '</div>';
|
|
3955
5912
|
html += '<div class="trace-search-stats">';
|
|
3956
5913
|
if (normalizedTraceQuery) {
|
|
@@ -3958,7 +5915,7 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
3958
5915
|
? ('Matched ' + matchedFlatIdxs.length + ' action' + (matchedFlatIdxs.length > 1 ? 's' : '') + ' in this session')
|
|
3959
5916
|
: 'No matches in this session';
|
|
3960
5917
|
} else {
|
|
3961
|
-
html += '
|
|
5918
|
+
html += esc(t('search_within_session'));
|
|
3962
5919
|
}
|
|
3963
5920
|
html += '</div>';
|
|
3964
5921
|
html += '</div>';
|
|
@@ -4002,7 +5959,7 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4002
5959
|
|
|
4003
5960
|
// Waterfall
|
|
4004
5961
|
if (normalizedTraceQuery && matchedFlatIdxs.length === 0) {
|
|
4005
|
-
html += '<div class="trace-search-empty">
|
|
5962
|
+
html += '<div class="trace-search-empty">' + esc(t('no_action_matched')) + ' "' + esc(traceSearchQuery) + '". ' + esc(t('try_hint')) + '</div>';
|
|
4006
5963
|
}
|
|
4007
5964
|
html += '<div class="waterfall">';
|
|
4008
5965
|
html += '<div class="wf-header"><span>Name</span><span style="text-align:right;padding-right:14px">Duration</span><span>Timeline</span></div>';
|
|
@@ -4026,9 +5983,6 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4026
5983
|
if (hasTreeParents) return a.flatIdx - b.flatIdx;
|
|
4027
5984
|
var startDiff = a.startMs - b.startMs;
|
|
4028
5985
|
if (startDiff !== 0) return startDiff;
|
|
4029
|
-
var priA = typePriority(a.action.action_type);
|
|
4030
|
-
var priB = typePriority(b.action.action_type);
|
|
4031
|
-
if (priA !== priB) return priA - priB;
|
|
4032
5986
|
var endDiff = a.endMs - b.endMs;
|
|
4033
5987
|
if (endDiff !== 0) return endDiff;
|
|
4034
5988
|
return a.idx - b.idx;
|
|
@@ -4041,6 +5995,10 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4041
5995
|
var shouldCollapseForSearch = normalizedTraceQuery && groupMatchIdxs.length === 0;
|
|
4042
5996
|
var shouldCollapseForLength = !normalizedTraceQuery && visibleGroupItems.length > TRACE_COLLAPSE_LIMIT;
|
|
4043
5997
|
var runDuration = group.rootSpan ? group.rootSpan.action.duration_ms : (groupEndMs - groupStartMs);
|
|
5998
|
+
var replaySpan = visibleGroupItems.find(function(span) {
|
|
5999
|
+
return !!(span && span.action && isReplayableAction(span.action));
|
|
6000
|
+
}) || null;
|
|
6001
|
+
var replayActionIdx = replaySpan ? replaySpan.flatIdx : -1;
|
|
4044
6002
|
var collapsedCls = (shouldCollapseForSearch || shouldCollapseForLength) ? ' collapsed' : '';
|
|
4045
6003
|
html += '<div class="wf-group-section' + collapsedCls + '" data-group-idx="' + groupIdx + '">';
|
|
4046
6004
|
html += '<div class="wf-group" onclick="toggleTraceGroup(' + groupIdx + ')">';
|
|
@@ -4050,6 +6008,10 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4050
6008
|
html += '<div class="wf-group-title-row">';
|
|
4051
6009
|
var runTitle = 'Trace ' + (groupIdx + 1);
|
|
4052
6010
|
html += '<span class="wf-group-title">' + runTitle + '</span>';
|
|
6011
|
+
var mergedSubagents = ensureArray(group && group.mergedSubagents);
|
|
6012
|
+
if (mergedSubagents.length > 0) {
|
|
6013
|
+
html += '<span class="wf-group-hit">+ subagent callback</span>';
|
|
6014
|
+
}
|
|
4053
6015
|
if (runDuration != null) {
|
|
4054
6016
|
html += '<span class="wf-group-hit">' + fmtDur(runDuration) + '</span>';
|
|
4055
6017
|
}
|
|
@@ -4059,17 +6021,28 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4059
6021
|
html += '</div>';
|
|
4060
6022
|
if (groupSnippet) {
|
|
4061
6023
|
html += '<div class="wf-group-snippet">' + highlightSearchText(groupSnippet, normalizedTraceQuery) + '</div>';
|
|
6024
|
+
} else if (mergedSubagents.length > 0) {
|
|
6025
|
+
var firstMerged = mergedSubagents[0];
|
|
6026
|
+
var mergedKey = firstMerged && firstMerged.childSessionKey ? shortSessionKey(firstMerged.childSessionKey) : '';
|
|
6027
|
+
html += '<div class="wf-group-snippet">Includes callback for ' + esc(mergedKey || 'subagent run') + '</div>';
|
|
4062
6028
|
} else if (group.rootSpan) {
|
|
4063
6029
|
html += '<div class="wf-group-snippet">' + esc(group.rootSpan.action.action_name) + ' completed this run</div>';
|
|
4064
6030
|
}
|
|
4065
6031
|
html += '</div>';
|
|
4066
6032
|
html += '</div>';
|
|
6033
|
+
html += '<div class="wf-group-right">';
|
|
6034
|
+
html += '<button class="wf-group-replay-btn" onclick="event.stopPropagation();openReplayFromAction(' + replayActionIdx + ')"'
|
|
6035
|
+
+ (replayActionIdx >= 0 ? '' : ' disabled')
|
|
6036
|
+
+ ' title="' + esc(replayActionIdx >= 0 ? tr('Replay this trace', '回放这个 Trace') : tr('No replayable LLM call in this trace', '该 Trace 没有可回放的 LLM 调用')) + '">'
|
|
6037
|
+
+ '↻ ' + esc(tr('Replay', '回放')) + '</button>';
|
|
4067
6038
|
html += '<span class="wf-group-meta">' + fmtTime(new Date(groupStartMs).toISOString()) + ' → ' + fmtTime(new Date(groupEndMs).toISOString()) + ' | ' + visibleGroupItems.length + ' actions</span>';
|
|
4068
6039
|
html += '</div>';
|
|
6040
|
+
html += '</div>';
|
|
4069
6041
|
|
|
4070
6042
|
visibleGroupItems.forEach(function(span) {
|
|
4071
6043
|
var a = span.action;
|
|
4072
6044
|
var color = typeColor(a.action_type);
|
|
6045
|
+
var displayActionName = formatTraceActionName(a);
|
|
4073
6046
|
var leftPct = ((span.startMs - groupStartMs) / groupTotalDur * 100).toFixed(2);
|
|
4074
6047
|
var widthPct = Math.max(((span.endMs - span.startMs) / groupTotalDur * 100), 0.3).toFixed(2);
|
|
4075
6048
|
var indent = span.level * 20;
|
|
@@ -4078,7 +6051,7 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4078
6051
|
html += '<div class="wf-name">';
|
|
4079
6052
|
if (indent > 0) html += '<span class="indent" style="width:' + indent + 'px;display:inline-flex;justify-content:center">└</span>';
|
|
4080
6053
|
html += '<span class="dot" style="background:' + color + '"></span>';
|
|
4081
|
-
html += '<span class="text">' + highlightSearchText(
|
|
6054
|
+
html += '<span class="text">' + highlightSearchText(displayActionName, normalizedTraceQuery) + '</span>';
|
|
4082
6055
|
html += '</div>';
|
|
4083
6056
|
html += '<div class="wf-dur">' + fmtDur(span.displayDurationMs != null ? span.displayDurationMs : a.duration_ms) + '</div>';
|
|
4084
6057
|
html += '<div class="wf-bar-wrap"><div class="wf-bar" style="left:' + leftPct + '%;width:' + widthPct + '%;background:' + color + '"></div></div>';
|
|
@@ -4151,7 +6124,7 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime, opts
|
|
|
4151
6124
|
app.innerHTML = renderLayout('trace',
|
|
4152
6125
|
'<div class="trace-header">' +
|
|
4153
6126
|
'<a href="#/traces" class="trace-back">← Back</a>' +
|
|
4154
|
-
'<div class="empty"><div class="icon">⚠️</div><div class="text">
|
|
6127
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">' + esc(t('failed_load')) + esc(String(err)) + '</div></div>' +
|
|
4155
6128
|
'</div>');
|
|
4156
6129
|
}
|
|
4157
6130
|
}
|
|
@@ -4293,27 +6266,30 @@ window.selectAction = function(fi, shouldScroll) {
|
|
|
4293
6266
|
// Header (sticky)
|
|
4294
6267
|
html += '<div class="detail-header">';
|
|
4295
6268
|
html += '<span class="dot" style="background:' + color + '"></span>';
|
|
4296
|
-
html += '<span class="name">' + esc(a
|
|
6269
|
+
html += '<span class="name">' + esc(formatTraceActionName(a)) + '</span>';
|
|
4297
6270
|
if (a.duration_ms != null) html += '<span class="dur">' + fmtDur(a.duration_ms) + '</span>';
|
|
4298
6271
|
html += '</div>';
|
|
4299
6272
|
|
|
4300
6273
|
// Meta
|
|
4301
6274
|
html += '<div class="detail-meta">';
|
|
4302
|
-
html += '<span><b>
|
|
4303
|
-
html += '<span><b>
|
|
4304
|
-
if (a.model_name) html += '<span><b>
|
|
4305
|
-
if (a.user_id) html += '<span><b>
|
|
4306
|
-
if (a.channel_id) html += '<span><b>
|
|
4307
|
-
if (a.prompt_tokens != null) html += '<span><b>
|
|
4308
|
-
if (a.completion_tokens != null) html += '<span><b>
|
|
6275
|
+
html += '<span><b>' + esc(t('type')) + ':</b> ' + typeLabel(a.action_type) + '</span>';
|
|
6276
|
+
html += '<span><b>' + esc(t('time')) + ':</b> ' + fmtTime(a.created_at) + '</span>';
|
|
6277
|
+
if (a.model_name) html += '<span><b>' + esc(t('model')) + ':</b> ' + esc(a.model_name) + '</span>';
|
|
6278
|
+
if (a.user_id) html += '<span><b>' + esc(t('agent')) + ':</b> ' + esc(a.user_id) + '</span>';
|
|
6279
|
+
if (a.channel_id) html += '<span><b>' + esc(t('channel')) + ':</b> ' + esc(a.channel_id) + '</span>';
|
|
6280
|
+
if (a.prompt_tokens != null) html += '<span><b>' + esc(t('prompt_tokens')) + ':</b> ' + a.prompt_tokens + '</span>';
|
|
6281
|
+
if (a.completion_tokens != null) html += '<span><b>' + esc(t('completion_tokens')) + ':</b> ' + a.completion_tokens + '</span>';
|
|
6282
|
+
var spawnInfo = getSessionsSpawnInfo(a);
|
|
6283
|
+
if (spawnInfo && spawnInfo.childSessionKey) {
|
|
6284
|
+
html += '<span><b>Child:</b> ' + esc(shortSessionKey(spawnInfo.childSessionKey)) + '</span>';
|
|
6285
|
+
}
|
|
4309
6286
|
html += '</div>';
|
|
4310
|
-
|
|
4311
6287
|
// Body: Input / Output side by side
|
|
4312
6288
|
html += '<div class="detail-body">';
|
|
4313
6289
|
var useStructuredView = !isToolActionType(a.action_type);
|
|
4314
6290
|
|
|
4315
6291
|
html += '<div class="detail-section">';
|
|
4316
|
-
html += '<h4>
|
|
6292
|
+
html += '<h4>' + esc(t('input')) + '</h4>';
|
|
4317
6293
|
if (input !== null && input !== undefined) {
|
|
4318
6294
|
if (useStructuredView) {
|
|
4319
6295
|
html += renderStructuredFields(input);
|
|
@@ -4336,7 +6312,7 @@ window.selectAction = function(fi, shouldScroll) {
|
|
|
4336
6312
|
html += '</div>';
|
|
4337
6313
|
|
|
4338
6314
|
html += '<div class="detail-section">';
|
|
4339
|
-
html += '<h4>
|
|
6315
|
+
html += '<h4>' + esc(t('output')) + '</h4>';
|
|
4340
6316
|
if (output !== null && output !== undefined) {
|
|
4341
6317
|
if (useStructuredView) {
|
|
4342
6318
|
html += renderStructuredFields(output);
|
|
@@ -4388,6 +6364,36 @@ function wireSparklinePointerDelegation() {
|
|
|
4388
6364
|
}
|
|
4389
6365
|
wireSparklinePointerDelegation();
|
|
4390
6366
|
|
|
6367
|
+
function wireMetricsPanelPointerDelegation() {
|
|
6368
|
+
function onMove(ev) {
|
|
6369
|
+
var t = ev.target;
|
|
6370
|
+
if (!t || !t.closest) return;
|
|
6371
|
+
var panel = t.closest('.metrics-panel-chart');
|
|
6372
|
+
if (!panel) return;
|
|
6373
|
+
var overlay = panel.querySelector('.metrics-panel-overlay');
|
|
6374
|
+
if (!overlay) return;
|
|
6375
|
+
metricsPanelMove(ev, overlay);
|
|
6376
|
+
}
|
|
6377
|
+
function onLeavePanel(ev) {
|
|
6378
|
+
var t = ev.target;
|
|
6379
|
+
if (!t || !t.closest) return;
|
|
6380
|
+
var panel = t.closest('.metrics-panel-chart');
|
|
6381
|
+
if (!panel) return;
|
|
6382
|
+
var rel = ev.relatedTarget;
|
|
6383
|
+
if (rel && panel.contains(rel)) return;
|
|
6384
|
+
var overlay = panel.querySelector('.metrics-panel-overlay');
|
|
6385
|
+
if (overlay) metricsPanelLeave(overlay);
|
|
6386
|
+
}
|
|
6387
|
+
if (typeof window.PointerEvent === 'function') {
|
|
6388
|
+
document.addEventListener('pointermove', onMove);
|
|
6389
|
+
document.addEventListener('pointerout', onLeavePanel, true);
|
|
6390
|
+
} else {
|
|
6391
|
+
document.addEventListener('mousemove', onMove);
|
|
6392
|
+
document.addEventListener('mouseout', onLeavePanel, true);
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
wireMetricsPanelPointerDelegation();
|
|
6396
|
+
|
|
4391
6397
|
/* ---------- metrics snapshot table: delegated handlers (embed / CSP-safe) ---------- */
|
|
4392
6398
|
document.addEventListener('click', function(ev) {
|
|
4393
6399
|
var tr = ev.target && ev.target.closest && ev.target.closest('#metrics-snapshot-tbody tr.metrics-row');
|
|
@@ -4404,8 +6410,47 @@ document.addEventListener('keydown', function(ev) {
|
|
|
4404
6410
|
metricsRowClick(tr);
|
|
4405
6411
|
});
|
|
4406
6412
|
|
|
6413
|
+
/* ---------- global crash guard (render error instead of blank screen) ---------- */
|
|
6414
|
+
function renderFatalOverlay(kind, message, stack) {
|
|
6415
|
+
var app = document.getElementById('app');
|
|
6416
|
+
if (!app) return;
|
|
6417
|
+
var safeMsg = esc(message || 'unknown error');
|
|
6418
|
+
var safeStack = esc(stack || '');
|
|
6419
|
+
app.innerHTML =
|
|
6420
|
+
'<div style="padding:24px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#f8fafc;background:#0b1220;min-height:100vh;box-sizing:border-box">' +
|
|
6421
|
+
'<h2 style="margin:0 0 12px 0;font-size:18px">Observability UI crashed</h2>' +
|
|
6422
|
+
'<div style="margin-bottom:10px;opacity:.85">Type: ' + esc(kind || 'error') + '</div>' +
|
|
6423
|
+
'<pre style="white-space:pre-wrap;word-break:break-word;background:#111827;border:1px solid #334155;border-radius:8px;padding:12px;line-height:1.5">' + safeMsg + '</pre>' +
|
|
6424
|
+
(safeStack ? ('<pre style="margin-top:10px;white-space:pre-wrap;word-break:break-word;background:#111827;border:1px solid #334155;border-radius:8px;padding:12px;line-height:1.4;max-height:42vh;overflow:auto">' + safeStack + '</pre>') : '') +
|
|
6425
|
+
'<div style="margin-top:12px;opacity:.75">Please refresh after update. If persists, send this screenshot.</div>' +
|
|
6426
|
+
'</div>';
|
|
6427
|
+
}
|
|
6428
|
+
window.addEventListener('error', function(ev) {
|
|
6429
|
+
var err = ev && ev.error ? ev.error : null;
|
|
6430
|
+
var msg = (ev && ev.message) ? ev.message : (err && err.message ? err.message : 'window error');
|
|
6431
|
+
var stack = err && err.stack ? String(err.stack) : '';
|
|
6432
|
+
renderFatalOverlay('window.error', String(msg || ''), stack);
|
|
6433
|
+
});
|
|
6434
|
+
window.addEventListener('unhandledrejection', function(ev) {
|
|
6435
|
+
var reason = ev ? ev.reason : null;
|
|
6436
|
+
var msg = '';
|
|
6437
|
+
var stack = '';
|
|
6438
|
+
if (reason && typeof reason === 'object') {
|
|
6439
|
+
msg = String(reason.message || reason.toString ? reason.toString() : '[object]');
|
|
6440
|
+
stack = String(reason.stack || '');
|
|
6441
|
+
} else {
|
|
6442
|
+
msg = String(reason || 'unhandled rejection');
|
|
6443
|
+
}
|
|
6444
|
+
renderFatalOverlay('unhandledrejection', msg, stack);
|
|
6445
|
+
});
|
|
6446
|
+
|
|
4407
6447
|
/* ---------- init ---------- */
|
|
4408
|
-
|
|
4409
|
-
|
|
6448
|
+
try {
|
|
6449
|
+
router();
|
|
6450
|
+
scheduleAlertBadgeLoad();
|
|
6451
|
+
} catch (e) {
|
|
6452
|
+
var err = e && typeof e === 'object' ? e : null;
|
|
6453
|
+
renderFatalOverlay('bootstrap', String(err && err.message ? err.message : e), String(err && err.stack ? err.stack : ''));
|
|
6454
|
+
}
|
|
4410
6455
|
`;
|
|
4411
6456
|
//# sourceMappingURL=ui.js.map
|