From d04d830c374dcd2cff9d34288e379a2a6ee76dd9 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Sat, 18 Jul 2026 12:03:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E3=80=8C=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=AF=84=E6=B5=8B=E3=80=8D=E9=A1=B5=E5=81=9A=E5=AE=9E=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E6=8E=A5=E7=9C=9F=E8=AF=84=E6=B5=8B?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=8C=E5=8E=BB=20mock=20(P1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审计 P1「admin 三页纯 mock」之一。此前 EvalsPage 是写死的质量趋势+编造的 错题本+虚构纠偏轨迹。现接 sundynix_eval 真数据(评测经 JetStream eval 流持久 落库,刚升级)。 - store/eval_query.go:EvalTrend(按天 avg 综合分/忠实度+低分计数)、EvalSummaryFor (ok/warn/poor/corrected 计数+均值)、PoorEvals(错题本,level in poor/warn + 评语+纠偏标记+租户名)。全 WithoutTenant 平台口径;忠实度均值只算 sources>0 (无来源的忠实度恒0会压低失真)。 - GET /admin/evals?days=(RequireAdmin);admin api.ts + EvalsPage 重写: 总览卡片(综合分/合格率/低分占比/纠偏采纳率)+质量&忠实度趋势(纯SVG折线+低分 背景条)+错题本(点行展开评语)。 - 诚实边界:纠偏「前后全文对照」后端未持久化,只存了 Reason/Corrected/各维度分, 故错题本展示评语+「已纠偏」标记,不再编造 before/after。 live:/admin/evals 返 57 次评测 avg=0.88、错题本10条、11天趋势;浏览器渲染 真数据(趋势线07-15真实下探)。go+tsc+41 vitest 全绿。 Co-Authored-By: Claude Opus 4.8 --- sundynix-admin/src/api.ts | 44 ++ sundynix-admin/src/pages/EvalsPage.tsx | 408 +++++++----------- sundynix-gateway/internal/handler/admin.go | 20 + sundynix-gateway/internal/router/router.go | 1 + sundynix-gateway/internal/store/eval_query.go | 97 +++++ 5 files changed, 329 insertions(+), 241 deletions(-) create mode 100644 sundynix-gateway/internal/store/eval_query.go diff --git a/sundynix-admin/src/api.ts b/sundynix-admin/src/api.ts index e985966..ee54d58 100644 --- a/sundynix-admin/src/api.ts +++ b/sundynix-admin/src/api.ts @@ -301,6 +301,50 @@ export async function adminReconcile(): Promise<{ diffs: ReconcileDiff[]; ok: bo return { diffs: d.diffs ?? [], ok: !!d.ok }; } +// ---- 自动评测观测(真数据,来自 sundynix_eval)---- +export interface EvalDay { + day: string; // YYYYMMDD + avg_overall: number; + avg_faithful: number; + count: number; + poor_count: number; +} +export interface EvalSummary { + total: number; + ok: number; + warn: number; + poor: number; + corrected: number; + avg_overall: number; +} +export interface PoorEval { + task_id: string; + tenant_name: string; + owner: string; + overall: number; + rule: number; + llm: number; + faithful: number; + level: string; + reason: string; + sources: number; + corrected: boolean; + created_at: string; +} + +export async function adminEvals(days = 14): Promise<{ from: string; to: string; trend: EvalDay[]; summary: EvalSummary; poor: PoorEval[] }> { + const res = guard(await fetch(`${ADMIN}/evals?days=${days}`, { headers: authHeaders() })); + const d = (await res.json().catch(() => ({}))) as { from?: string; to?: string; trend?: EvalDay[]; summary?: EvalSummary; poor?: PoorEval[]; error?: string }; + if (!res.ok) throw new Error(d.error ?? `evals failed: ${res.status}`); + return { + from: d.from ?? "", + to: d.to ?? "", + trend: d.trend ?? [], + summary: d.summary ?? { total: 0, ok: 0, warn: 0, poor: 0, corrected: 0, avg_overall: 0 }, + poor: d.poor ?? [], + }; +} + // gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。 export async function gatewayOnline(): Promise { try { diff --git a/sundynix-admin/src/pages/EvalsPage.tsx b/sundynix-admin/src/pages/EvalsPage.tsx index b15ce3d..4a355d9 100644 --- a/sundynix-admin/src/pages/EvalsPage.tsx +++ b/sundynix-admin/src/pages/EvalsPage.tsx @@ -1,258 +1,184 @@ -import { useState } from "react"; +import { Fragment, useEffect, useState, type ReactNode } from "react"; +import { adminEvals, type EvalDay, type EvalSummary, type PoorEval } from "../api"; -// Mock 评测趋势数据 -const QUALITY_TREND = [0.82, 0.84, 0.79, 0.81, 0.85, 0.88, 0.89, 0.87, 0.86, 0.91, 0.92, 0.88, 0.87, 0.88]; -const HALLUCINATION_TREND = [18, 15, 22, 19, 14, 11, 10, 12, 13, 8, 7, 11, 12, 10]; // % 比例 -const DATES = ["06-14", "06-15", "06-16", "06-17", "06-18", "06-19", "06-20", "06-21", "06-22", "06-23", "06-24", "06-25", "06-26", "06-27"]; - -// Mock 低分评测记录(错题本)与纠偏轨迹 -const MOCK_POOR_RUNS = [ - { - id: "task_e8f2a1b9", - time: "11:47:05", - user: "Bob", - agentName: "法律合同审查 Agent", - overall: 0.42, - ruleScore: 0.60, - llmScore: 0.50, - faithful: 0.15, - level: "poor", - reason: "幻觉严重。模型声称合同中包含‘三年内无条件退款限制条款’,但所附 RAG 参考材料中仅提及‘按比例折算退款规则’,属于严重的知识库脱轨和无中生有(无立足依据)。", - trace: { - initialAnswer: "根据合同第 4 条,本合同包含三年内无条件全额退款条款,客户可随时申请解除合作。", - critique: "【评测发现异常】RAG 知识块[文档: 退款政策_2026.pdf]中明确规定退款须‘按合作月份比例折算,扣除已产生渠道服务费后退还余款,且需提前30天书面申请’。模型回答‘无条件全额退款’属严重事实性捏造,忠实度(Faithful)分值判定为 0.15。", - refinePrompt: "你是一个严肃的合同审查纠偏助手。在前一次生成中,模型产生了事实性幻觉。请根据参考材料【退款政策_2026.pdf】:‘退款须按合作月份比例折算,扣除已产生渠道服务费后退还余款,且需提前30天书面申请’,对前次答案【根据合同第 4 条...】进行修改纠正,必须忠实于材料,杜绝捏造无条件条款。", - refinedAnswer: "根据退款政策附件规则,退款非无条件全额,而是必须按合作月份比例折算,且扣除已产生渠道服务费后退还余款。此外,客户申请退款需提前30天提交书面申请,合同第4条仅规定了申请路径,而非‘无条件退款’。", - newScore: 0.85 - } - }, - { - id: "task_ff3c0b12", - time: "10:12:30", - user: "Alice", - agentName: "医学文献总结 Agent", - overall: 0.55, - ruleScore: 0.50, - llmScore: 0.60, - faithful: 0.55, - level: "poor", - reason: "引用缺失。用户要求‘回答时必须在事实断言处标注 RAG 参考的文档来源’,但模型生成的总结文本中未包含任何形如 [1] 或 [doc_xxx] 的引用锚点,违反了输入 DSL 的强制规范规则。", - trace: { - initialAnswer: "该临床研究表明,使用该抗体偶联药物能提高 15% 的无进展生存期(PFS),且中位缓解期达到了 12.4 个月。", - critique: "【评测发现异常】模型得出了准确的结论,但未按照强制指令将数据来源指向知识块 [文献_ADC研究_Phase3.pdf]。违反强制引用约束,规则分降低为 0.50。", - refinePrompt: "前次生成中未包含数据来源引用。请在前次总结中,为‘提高 15% 的无进展生存期’与‘中位缓解期达到 12.4 个月’数据,在行内添加形如 [文献_ADC研究_Phase3.pdf] 的数据来源引用标签。", - refinedAnswer: "最新临床三期研究表明,使用该抗体偶联药物能提高 15% 的无进展生存期(PFS)[文献_ADC研究_Phase3.pdf],且中位缓解期达到了 12.4 个月[文献_ADC研究_Phase3.pdf]。", - newScore: 0.92 - } - }, - { - id: "task_a5fd2510", - time: "09:30:15", - user: "Alice", - agentName: "财务审计核对 Agent", - overall: 0.48, - ruleScore: 0.40, - llmScore: 0.55, - faithful: 0.50, - level: "poor", - reason: "计算口径不一致。知识库给出的 2025 Q4 营业收入为‘1.2 亿元(含未结算款项)’,但财务报表核查中模型漏计了未结算款,算出了 1.05 亿元,与账面发生额不符。", - trace: { - initialAnswer: "核对完毕,Q4 累计核算收入为 1.05 亿元,与系统申报一致。", - critique: "【评测发现异常】少计算了未结算款项 1500 万元。知识库规定必须将未结算款项计入 Q4 营业收入内。规则评分降为 0.40。", - refinePrompt: "计算遗漏。请把财务参考中的‘1500万未结算款项’加进 Q4 营业收入核对中,重新计算总营收并核对金额。", - refinedAnswer: "重新核算后,Q4 累计核算总收入为 1.2 亿元(已包含 1500 万元未结算款项),与系统申报发生额完全吻合。", - newScore: 0.89 - } - } -]; +// 自动评测观测:真数据(来自 sundynix_eval,评测经 JetStream eval 流持久落库)。 +// 质量趋势 + 计数总览 + 错题本(低分评测 + 评语 + 纠偏标记)。 +// 注:纠偏前后全文轨迹后端未持久化,错题本展示评语(Reason)与「已纠偏」标记,不含 before/after 对照。 +const pct = (v: number) => `${Math.round(v * 100)}%`; +const LEVEL_BADGE: Record = { poor: "bg-rose-50 text-rose-600", warn: "bg-amber-50 text-amber-600", ok: "bg-emerald-50 text-emerald-600" }; +const LEVEL_LABEL: Record = { poor: "低分", warn: "警告", ok: "合格" }; +const mmdd = (ymd: string) => (ymd.length === 8 ? `${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` : ymd); export function EvalsPage() { - const [runs, setRuns] = useState(MOCK_POOR_RUNS); - const [expandedId, setExpandedId] = useState(null); + const [days, setDays] = useState(14); + const [trend, setTrend] = useState([]); + const [summary, setSummary] = useState(null); + const [poor, setPoor] = useState([]); + const [open, setOpen] = useState(null); // 展开评语的 task_id + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(""); - const toggleExpand = (id: string) => { - setExpandedId((prev) => (prev === id ? null : id)); - }; + useEffect(() => { + setLoading(true); + adminEvals(days) + .then((r) => { + setTrend(r.trend); + setSummary(r.summary); + setPoor(r.poor); + setErr(""); + }) + .catch((e) => setErr((e as Error).message)) + .finally(() => setLoading(false)); + }, [days]); - // SVG 趋势图宽高 - const chartW = 260; - const chartH = 70; - const pad = 10; + if (loading) return
加载评测数据中…
; + if (err) return
评测加载失败:{err}
; - // 1. 质量曲线点计算 - const maxValQ = 1.0; - const pointsQ = QUALITY_TREND.map((val, idx) => { - const x = pad + (idx * (chartW - pad * 2)) / (QUALITY_TREND.length - 1); - const y = chartH - pad - (val * (chartH - pad * 2)) / maxValQ; - return { x, y }; - }); - const pathQ = pointsQ.reduce((p, pt, i) => p + `${i === 0 ? "M" : "L"} ${pt.x.toFixed(1)} ${pt.y.toFixed(1)}`, ""); - - // 2. 幻觉率曲线点计算 - const maxValH = 30; // 最大 30% 刻度 - const pointsH = HALLUCINATION_TREND.map((val, idx) => { - const x = pad + (idx * (chartW - pad * 2)) / (HALLUCINATION_TREND.length - 1); - const y = chartH - pad - (val * (chartH - pad * 2)) / maxValH; - return { x, y }; - }); - const pathH = pointsH.reduce((p, pt, i) => p + `${i === 0 ? "M" : "L"} ${pt.x.toFixed(1)} ${pt.y.toFixed(1)}`, ""); + const s = summary!; + const correctRate = s.poor + s.warn > 0 ? s.corrected / (s.poor + s.warn) : 0; return (
- {/* 顶部大盘指标与微缩趋势图 */} -
- {/* 指标 1:综合评测均分 */} -
-
- 自动化综合均分 -

0.88

- 本周均值较上周 ↑ 3% -
- {/* 微型折线图 */} -
- - - -
-
- - {/* 指标 2:忠实度评测与幻觉率 */} -
-
- 平均幻觉发生率 -

10%

- 较本月初降低 ↓ 8% -
- {/* 微型折线图 */} -
- - - -
-
- - {/* 指标 3:纠偏系统效能 */} -
- 低分纠偏成功率 (恒温器效能) -
-

84.2%

- 纠偏重生成共计 228 次 -
-
-
-
-
+
+
+

自动评测

+

质量趋势 · 忠实度 · 低分错题本与纠偏成效(全平台真实评测)

+
+
+ {[7, 14, 30].map((d) => ( + + ))} +
- {/* 自动纠偏错题本(失败记录) */} -
-
-

自动纠偏错题本 (质量异常事件)

-

罗列所有触发低分(poor)警告的请求,可点击展开查看系统对答案的自动纠正(Refinement)全轨迹

+ {/* 总览计数 */} +
+ + + + +
+ + {/* 质量 & 忠实度趋势 */} +
+
+

质量与忠实度趋势

+
+ 综合分 + 忠实度 + 低分数 +
+ +
-
- {runs.map((r) => { - const isExpanded = expandedId === r.id; - return ( -
- {/* 简要行 */} -
toggleExpand(r.id)} - className={`flex flex-col md:flex-row md:items-center justify-between gap-4 p-3.5 cursor-pointer hover:bg-gray-50/50 transition-colors ${ - isExpanded ? "bg-violet-50/20 border-b border-violet-100" : "" - }`} - > -
- [{r.time}] -
-
{r.agentName}
-
提交者: {r.user} | ID: {r.id}
-
-
- - {/* 分数指标组 */} -
-
-
综合得分
-
{r.overall.toFixed(2)}
-
-
-
规则约束
-
{r.ruleScore.toFixed(2)}
-
-
-
RAG忠实度
-
- {r.faithful.toFixed(2)} -
-
-
-
纠偏后分数
-
↑ {r.trace.newScore.toFixed(2)}
-
- - {/* 展开折叠箭头 */} - {isExpanded ? "▲" : "▼"} -
-
- - {/* 展开详细信息(纠偏对齐轨迹详情) */} - {isExpanded && ( -
- {/* 1. 问题定位 */} -
-
评测问题诊断 (Evaluator Diagnosis)
-

{r.reason}

-
- - {/* 2. 纠偏流转卡片组 */} -
- {/* 初次答案 */} -
-
- - 初次生成(存在幻觉/缺陷) Score: {r.overall.toFixed(2)} -
-

{r.trace.initialAnswer}

-
- - {/* 初次评测评语 */} -
-
- - 评测诊断意见 (Evaluator Critique) -
-

{r.trace.critique}

-
- - {/* 纠偏 Prompt 注入 */} -
-
- - 纠偏重写指令 (Refinement Rewrite Prompt) -
-

- {r.trace.refinePrompt} -

-
- - {/* 纠偏后最终答案 */} -
-
- - 纠偏重写采纳(纠偏成功) Score: {r.trace.newScore.toFixed(2)} -
-

{r.trace.refinedAnswer}

-
-
-
- )} -
- ); - })} + {/* 错题本 */} +
+
+

错题本(低分/警告评测)

+ 点行展开评语;「已纠偏」= 自动重生成后采纳
-
+ {poor.length === 0 ? ( +
区间内没有低分评测 —— 质量良好
+ ) : ( +
+ + + + + + + + + + + + + + {poor.map((r) => ( + + setOpen(open === r.task_id ? null : r.task_id)} + className="cursor-pointer border-b border-gray-50 hover:bg-gray-50"> + + + + + + + + + {open === r.task_id && ( + + + + )} + + ))} + +
时间任务 / 租户综合规则质量忠实等级
{r.created_at} +
{r.task_id}
+
{r.tenant_name || "—"}
+
{pct(r.overall)}{pct(r.rule)}{pct(r.llm)}{r.sources > 0 ? pct(r.faithful) : "—"} +
+ {LEVEL_LABEL[r.level] ?? r.level} + {r.corrected && 已纠偏} +
+
+
评测评语
+

{r.reason || "(无评语)"}

+
检索来源 {r.sources} 处 · 提交者 {r.owner || "—"}
+
+
+ )} +
+
+ ); +} + +const TONE: Record = { violet: "text-violet-600", emerald: "text-emerald-600", rose: "text-rose-500", cyan: "text-cyan-600" }; +function Stat({ label, value, sub, tone }: { label: string; value: ReactNode; sub?: ReactNode; tone: string }) { + return ( +
+
{label}
+
{value}
+ {sub &&
{sub}
} +
+ ); +} + +// TrendChart:综合分/忠实度折线(左轴 [0,1])+ 低分条数背景条。纯 SVG,无依赖。 +function TrendChart({ trend }: { trend: EvalDay[] }) { + if (trend.length === 0) return
区间内暂无评测
; + const w = 720, h = 160, pad = 24; + const n = trend.length; + const x = (i: number) => pad + (n === 1 ? (w - 2 * pad) / 2 : (i * (w - 2 * pad)) / (n - 1)); + const y = (v: number) => h - pad - v * (h - 2 * pad); + const maxPoor = Math.max(1, ...trend.map((d) => d.poor_count)); + const line = (get: (d: EvalDay) => number) => trend.map((d, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(get(d)).toFixed(1)}`).join(" "); + + return ( +
+ + {[0, 0.5, 1].map((g) => ( + + + {g} + + ))} + {/* 低分条数背景条 */} + {trend.map((d, i) => ( + + ))} + d.avg_overall)} fill="none" stroke="#7c3aed" strokeWidth={2} /> + d.avg_faithful || 0)} fill="none" stroke="#06b6d4" strokeWidth={2} strokeDasharray="3 2" /> + {trend.map((d, i) => ( + {mmdd(d.day)} + ))} +
); } diff --git a/sundynix-gateway/internal/handler/admin.go b/sundynix-gateway/internal/handler/admin.go index 4447fc0..4b6c724 100644 --- a/sundynix-gateway/internal/handler/admin.go +++ b/sundynix-gateway/internal/handler/admin.go @@ -570,3 +570,23 @@ func (h *Handler) AdminUsage(c *gin.Context) { } c.JSON(http.StatusOK, resp) } + +// AdminEvals: GET /api/v1/admin/evals?days= —— 自动评测观测(趋势 + 计数 + 错题本)。全平台口径。 +// 数据来自 sundynix_eval(评测经 JetStream eval 流持久落库);此前该页纯 mock。 +func (h *Handler) AdminEvals(c *gin.Context) { + ctx := store.WithoutTenant(c.Request.Context()) + now := time.Now() + days := 14 + if d, err := strconv.Atoi(c.Query("days")); err == nil && d > 0 && d <= 90 { + days = d + } + from := now.AddDate(0, 0, -(days - 1)).Format("20060102") + to := now.Format("20060102") + c.JSON(http.StatusOK, gin.H{ + "from": from, + "to": to, + "trend": h.db.EvalTrend(ctx, from, to), + "summary": h.db.EvalSummaryFor(ctx, from, to), + "poor": h.db.PoorEvals(ctx, 30), + }) +} diff --git a/sundynix-gateway/internal/router/router.go b/sundynix-gateway/internal/router/router.go index 37316bd..5cb0776 100644 --- a/sundynix-gateway/internal/router/router.go +++ b/sundynix-gateway/internal/router/router.go @@ -155,6 +155,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob. admin.GET("/status", h.AdminStatus) // 服务状态:基建/服务探活 + MCP 工具注册 admin.GET("/overview", h.AdminOverview) // 系统级聚合:全平台用户/任务/评测/模型态/提示词态/健康 admin.GET("/usage", h.AdminUsage) // 用量/积分/成本:全平台按天趋势 + 租户排行 / 单租户余额 + admin.GET("/evals", h.AdminEvals) // 自动评测观测:质量趋势 + 计数 + 错题本(真数据) admin.POST("/migrate-kb-storage", h.MigrateKBStorage) // 增量3:存量 KB 三库 owner/kb→space/kb 重灌(一次性) admin.GET("/audit", h.AuditList) // 敏感操作审计流(倒序,翻页) admin.GET("/guardrail-events", h.GuardrailEvents) // 护栏命中安全事件流(倒序,翻页) diff --git a/sundynix-gateway/internal/store/eval_query.go b/sundynix-gateway/internal/store/eval_query.go new file mode 100644 index 0000000..f2d87eb --- /dev/null +++ b/sundynix-gateway/internal/store/eval_query.go @@ -0,0 +1,97 @@ +package store + +import "context" + +// 评测观测查询(admin「自动评测」页做实用;此前该页纯 mock)。全平台口径 → WithoutTenant。 +// 注:评测的「纠偏前后全文轨迹」后端未持久化,只存了 Reason(评语)/Corrected(是否已纠偏采纳)/ +// 各维度分,故错题本展示这些真数据,不含编造的 before/after 对照。 + +// EvalDay 是评测趋势按天一行。 +type EvalDay struct { + Day string `json:"day"` // YYYYMMDD + AvgOverall float64 `json:"avg_overall"` // 当日综合分均值 [0,1] + AvgFaithful float64 `json:"avg_faithful"` // 当日忠实度均值(仅计有来源的评测) + Count int64 `json:"count"` + PoorCount int64 `json:"poor_count"` // 当日 poor 级条数(幻觉/低质趋势) +} + +// EvalTrend 按天聚合评测(from/to 为 YYYYMMDD)。avg_faithful 只算有检索来源的评测(sources>0), +// 无来源的忠实度恒 0 会把均值压低失真。 +func (p *Postgres) EvalTrend(ctx context.Context, from, to string) []EvalDay { + if p.db == nil { + return nil + } + var out []EvalDay + p.db.WithContext(ctx).Model(&Eval{}). + Select("to_char(created_at,'YYYYMMDD') as day, "+ + "avg(overall) as avg_overall, "+ + "avg(case when sources > 0 then faithful end) as avg_faithful, "+ + "count(*) as count, "+ + "count(case when level = 'poor' then 1 end) as poor_count"). + Where("to_char(created_at,'YYYYMMDD') >= ? AND to_char(created_at,'YYYYMMDD') <= ?", from, to). + Group("day").Order("day").Scan(&out) + return out +} + +// EvalSummary 是评测总览计数。 +type EvalSummary struct { + Total int64 `json:"total"` + OK int64 `json:"ok"` + Warn int64 `json:"warn"` + Poor int64 `json:"poor"` + Corrected int64 `json:"corrected"` // 经低分自动纠偏重生成后采纳的条数(恒温器闭环成效) + AvgOverall float64 `json:"avg_overall"` // 区间综合分均值 +} + +// EvalSummaryFor 区间内评测计数(from/to 为 YYYYMMDD)。 +func (p *Postgres) EvalSummaryFor(ctx context.Context, from, to string) EvalSummary { + var s EvalSummary + if p.db == nil { + return s + } + p.db.WithContext(ctx).Model(&Eval{}). + Select("count(*) as total, "+ + "count(case when level='ok' then 1 end) as ok, "+ + "count(case when level='warn' then 1 end) as warn, "+ + "count(case when level='poor' then 1 end) as poor, "+ + "count(case when corrected then 1 end) as corrected, "+ + "coalesce(avg(overall),0) as avg_overall"). + Where("to_char(created_at,'YYYYMMDD') >= ? AND to_char(created_at,'YYYYMMDD') <= ?", from, to). + Scan(&s) + return s +} + +// PoorEval 是错题本一行(低分评测 + 评语 + 纠偏标记;带租户名免前端二次查)。 +type PoorEval struct { + TaskID string `json:"task_id"` + TenantName string `json:"tenant_name"` + Owner string `json:"owner"` + Overall float64 `json:"overall"` + Rule float64 `json:"rule"` + LLM float64 `json:"llm"` + Faithful float64 `json:"faithful"` + Level string `json:"level"` + Reason string `json:"reason"` + Sources int `json:"sources"` + Corrected bool `json:"corrected"` + CreatedAt string `json:"created_at"` +} + +// PoorEvals 最近的低分评测(level=poor/warn,错题本)。 +func (p *Postgres) PoorEvals(ctx context.Context, limit int) []PoorEval { + if p.db == nil { + return nil + } + if limit <= 0 || limit > 100 { + limit = 30 + } + var out []PoorEval + p.db.WithContext(ctx).Table("sundynix_eval e"). + Select("e.task_id, coalesce(t.name,'') as tenant_name, e.owner, e.overall, e.rule, e.llm, "+ + "e.faithful, e.level, e.reason, e.sources, e.corrected, "+ + "to_char(e.created_at,'YYYY-MM-DD HH24:MI') as created_at"). + Joins("left join sundynix_tenant t on t.id = e.tenant_id"). + Where("e.level in ('poor','warn') AND e.deleted_at IS NULL"). + Order("e.created_at desc").Limit(limit).Scan(&out) + return out +}