mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-20 13:37:36 +08:00
webui ekg: add ekg stats api and task-audit insights panel (top errsig/provider, escalation count)
This commit is contained in:
@@ -93,6 +93,7 @@ func (s *RegistryServer) Start(ctx context.Context) error {
|
|||||||
mux.HandleFunc("/webui/api/task_queue", s.handleWebUITaskQueue)
|
mux.HandleFunc("/webui/api/task_queue", s.handleWebUITaskQueue)
|
||||||
mux.HandleFunc("/webui/api/tasks", s.handleWebUITasks)
|
mux.HandleFunc("/webui/api/tasks", s.handleWebUITasks)
|
||||||
mux.HandleFunc("/webui/api/task_daily_summary", s.handleWebUITaskDailySummary)
|
mux.HandleFunc("/webui/api/task_daily_summary", s.handleWebUITaskDailySummary)
|
||||||
|
mux.HandleFunc("/webui/api/ekg_stats", s.handleWebUIEKGStats)
|
||||||
mux.HandleFunc("/webui/api/exec_approvals", s.handleWebUIExecApprovals)
|
mux.HandleFunc("/webui/api/exec_approvals", s.handleWebUIExecApprovals)
|
||||||
mux.HandleFunc("/webui/api/logs/stream", s.handleWebUILogsStream)
|
mux.HandleFunc("/webui/api/logs/stream", s.handleWebUILogsStream)
|
||||||
mux.HandleFunc("/webui/api/logs/recent", s.handleWebUILogsRecent)
|
mux.HandleFunc("/webui/api/logs/recent", s.handleWebUILogsRecent)
|
||||||
@@ -1617,6 +1618,93 @@ func (s *RegistryServer) handleWebUITaskDailySummary(w http.ResponseWriter, r *h
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "date": date, "report": report})
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "date": date, "report": report})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RegistryServer) handleWebUIEKGStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !s.checkAuth(r) {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
workspace := strings.TrimSpace(s.workspacePath)
|
||||||
|
ekgPath := filepath.Join(workspace, "memory", "ekg-events.jsonl")
|
||||||
|
b, _ := os.ReadFile(ekgPath)
|
||||||
|
lines := strings.Split(string(b), "\n")
|
||||||
|
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||||
|
lines = lines[:len(lines)-1]
|
||||||
|
}
|
||||||
|
if len(lines) > 3000 {
|
||||||
|
lines = lines[len(lines)-3000:]
|
||||||
|
}
|
||||||
|
providerScore := map[string]float64{}
|
||||||
|
errSigCount := map[string]int{}
|
||||||
|
for _, ln := range lines {
|
||||||
|
if strings.TrimSpace(ln) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var row map[string]interface{}
|
||||||
|
if json.Unmarshal([]byte(ln), &row) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
provider := strings.TrimSpace(fmt.Sprintf("%v", row["provider"]))
|
||||||
|
status := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", row["status"])))
|
||||||
|
errSig := strings.TrimSpace(fmt.Sprintf("%v", row["errsig"]))
|
||||||
|
if provider != "" {
|
||||||
|
switch status {
|
||||||
|
case "success":
|
||||||
|
providerScore[provider] += 1
|
||||||
|
case "suppressed":
|
||||||
|
providerScore[provider] += 0.2
|
||||||
|
case "error":
|
||||||
|
providerScore[provider] -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errSig != "" {
|
||||||
|
errSigCount[errSig]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type kv struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Score float64 `json:"score,omitempty"`
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
}
|
||||||
|
providerTop := make([]kv, 0, len(providerScore))
|
||||||
|
for k, v := range providerScore {
|
||||||
|
providerTop = append(providerTop, kv{Key: k, Score: v})
|
||||||
|
}
|
||||||
|
sort.Slice(providerTop, func(i, j int) bool { return providerTop[i].Score > providerTop[j].Score })
|
||||||
|
if len(providerTop) > 5 {
|
||||||
|
providerTop = providerTop[:5]
|
||||||
|
}
|
||||||
|
errTop := make([]kv, 0, len(errSigCount))
|
||||||
|
for k, v := range errSigCount {
|
||||||
|
errTop = append(errTop, kv{Key: k, Count: v})
|
||||||
|
}
|
||||||
|
sort.Slice(errTop, func(i, j int) bool { return errTop[i].Count > errTop[j].Count })
|
||||||
|
if len(errTop) > 5 {
|
||||||
|
errTop = errTop[:5]
|
||||||
|
}
|
||||||
|
escalations := 0
|
||||||
|
tasksPath := filepath.Join(workspace, "memory", "tasks.json")
|
||||||
|
if tb, err := os.ReadFile(tasksPath); err == nil {
|
||||||
|
var tasks []map[string]interface{}
|
||||||
|
if json.Unmarshal(tb, &tasks) == nil {
|
||||||
|
for _, t := range tasks {
|
||||||
|
if strings.TrimSpace(fmt.Sprintf("%v", t["block_reason"])) == "repeated_error_signature" {
|
||||||
|
escalations++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
"provider_top": providerTop,
|
||||||
|
"errsig_top": errTop,
|
||||||
|
"escalation_count": escalations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RegistryServer) handleWebUITasks(w http.ResponseWriter, r *http.Request) {
|
func (s *RegistryServer) handleWebUITasks(w http.ResponseWriter, r *http.Request) {
|
||||||
if !s.checkAuth(r) {
|
if !s.checkAuth(r) {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
|
|
||||||
|
type EKGKV = { key?: string; score?: number; count?: number };
|
||||||
|
|
||||||
type TaskAuditItem = {
|
type TaskAuditItem = {
|
||||||
task_id?: string;
|
task_id?: string;
|
||||||
time?: string;
|
time?: string;
|
||||||
@@ -37,6 +39,9 @@ const TaskAudit: React.FC = () => {
|
|||||||
const [dailyReport, setDailyReport] = useState<string>('');
|
const [dailyReport, setDailyReport] = useState<string>('');
|
||||||
const [reportDate, setReportDate] = useState<string>(new Date().toISOString().slice(0,10));
|
const [reportDate, setReportDate] = useState<string>(new Date().toISOString().slice(0,10));
|
||||||
const [showDailyReport, setShowDailyReport] = useState(false);
|
const [showDailyReport, setShowDailyReport] = useState(false);
|
||||||
|
const [ekgProviderTop, setEkgProviderTop] = useState<EKGKV[]>([]);
|
||||||
|
const [ekgErrsigTop, setEkgErrsigTop] = useState<EKGKV[]>([]);
|
||||||
|
const [ekgEscalationCount, setEkgEscalationCount] = useState<number>(0);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -57,6 +62,14 @@ const TaskAudit: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
setDailyReport('');
|
setDailyReport('');
|
||||||
}
|
}
|
||||||
|
const ekgUrl = `/webui/api/ekg_stats${q || ''}`;
|
||||||
|
const er = await fetch(ekgUrl);
|
||||||
|
if (er.ok) {
|
||||||
|
const ej = await er.json();
|
||||||
|
setEkgProviderTop(Array.isArray(ej.provider_top) ? ej.provider_top : []);
|
||||||
|
setEkgErrsigTop(Array.isArray(ej.errsig_top) ? ej.errsig_top : []);
|
||||||
|
setEkgEscalationCount(Number(ej.escalation_count || 0));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setItems([]);
|
setItems([]);
|
||||||
@@ -125,6 +138,32 @@ const TaskAudit: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-3 text-sm">
|
||||||
|
<div className="text-xs text-zinc-400 uppercase tracking-wider mb-2">EKG Insights</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
||||||
|
<div className="rounded-lg border border-zinc-800 bg-zinc-950/40 p-2">
|
||||||
|
<div className="text-zinc-500 mb-1">Escalations</div>
|
||||||
|
<div className="text-zinc-100 text-base font-semibold">{ekgEscalationCount}</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-zinc-800 bg-zinc-950/40 p-2 md:col-span-2">
|
||||||
|
<div className="text-zinc-500 mb-1">Top Providers</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{ekgProviderTop.length === 0 ? <div className="text-zinc-500">-</div> : ekgProviderTop.map((x, i) => (
|
||||||
|
<div key={`${x.key}-${i}`} className="text-zinc-200">{x.key} <span className="text-zinc-500">({Number(x.score || 0).toFixed(2)})</span></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 rounded-lg border border-zinc-800 bg-zinc-950/40 p-2 text-xs">
|
||||||
|
<div className="text-zinc-500 mb-1">Top Error Signatures</div>
|
||||||
|
<div className="space-y-1 max-h-24 overflow-y-auto">
|
||||||
|
{ekgErrsigTop.length === 0 ? <div className="text-zinc-500">-</div> : ekgErrsigTop.map((x, i) => (
|
||||||
|
<div key={`${x.key}-${i}`} className="text-zinc-200 truncate">{x.key} <span className="text-zinc-500">(x{x.count || 0})</span></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-3 text-sm">
|
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-3 text-sm">
|
||||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('dailySummary')}</div>
|
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('dailySummary')}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user