Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c413cc997 | ||
|
|
f499144465 |
@@ -163,6 +163,9 @@
|
||||
### `DELETE /api/tests`
|
||||
一键清空全部测试历史(测试记录 + 采样指标 + 日志),不可恢复。响应 `{ "ok": true }`。
|
||||
|
||||
### `GET /api/tests/export`
|
||||
一键导出全部历史记录为 JSON 文件下载(`Content-Disposition: attachment`,文件名带时间戳)。每条含基本字段 + 配置(**API Key 打码为 `******`**)+ 生成参数 + 汇总指标,不含逐条采样与完整日志。
|
||||
|
||||
### `POST /api/tests/<id>/cancel`
|
||||
停止正在运行的测试。响应 `{ "ok": true, "msg": "正在停止..." }`
|
||||
|
||||
@@ -369,6 +372,8 @@ curl "$BASE/api/tests?page=1&page_size=20"
|
||||
curl "$BASE/api/tests?q=Qwen&status=done,error&sort=decode&order=desc&page=1&page_size=10"
|
||||
# 一键清空全部历史(不可恢复)
|
||||
curl -X DELETE $BASE/api/tests
|
||||
# 一键导出全部历史(JSON 文件下载)
|
||||
curl -OJ $BASE/api/tests/export
|
||||
curl $BASE/api/tests/9
|
||||
|
||||
# 画图数据(CSV)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- **访问地址:** `http://<IP>:16097/`
|
||||
- **技术栈:** Python 3 + Flask + SQLite(纯 REST,无额外依赖)
|
||||
- **版本:** v2.6.0
|
||||
- **版本:** v2.7.0
|
||||
|
||||
---
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
- **文件下载 Excel(xlsx)**:历史记录「Excel」按钮或详情弹窗「导出 Excel」,包含 汇总 / 采样明细 / 日志 三个 Sheet
|
||||
- **文件下载 JSON**:详情弹窗「导出 JSON」
|
||||
- 测试历史留存(含测试名称),可随时刷新、查看、导出、删除
|
||||
- **📥 一键导出全部历史**:历史卡片「导出全部」按钮,把全部测试记录(配置 Key 打码 + 生成参数 + 汇总指标)导出为一个 JSON 文件下载(`GET /api/tests/export`)
|
||||
- **📄 历史分页 + 筛选 + 搜索 + 排序**:历史列表支持分页(10/20/50/100 条每页)、多条件筛选(状态多选、提供商)、关键词搜索(编号/名称/模型/提供商/状态/时间)、点击表头升降排序(#/时间/名称/提供商/模型/采样/首字/预填充/解码/状态,数值列空值恒排最后);提供「✕ 重置」一键恢复默认
|
||||
- **🗑 一键清空历史**:历史卡片右上角「清空历史」二次确认后,删除全部测试记录/采样/日志(不可恢复)
|
||||
- **⬆⬇ 一键置顶/置底**:右下角固定悬浮按钮,平滑滚动到页面顶部/底部
|
||||
- **🗜 左侧配置块可折叠**:大模型接口配置 / 速度测试配置 / 评测站同步 三个卡片点击标题即可折叠/展开(箭头指示),状态本地持久化
|
||||
- **⬆⬇ 一键置顶/置底**:右下角固定悬浮按钮(亮蓝渐变高对比配色,易于发现),平滑滚动到页面顶部/底部
|
||||
- **⚖️ 多测试结果对比**:测试历史表格勾选多个测试(可全选),点「⚖️ 对比所选」弹出对比面板:
|
||||
- **指标对比表**:时间 / 名称 / 模型 / 并发档 / 采样 / 首字 / 预填充 / 解码 / 单流均解码 / 输出 / 总耗时 并排展示(点击测试名可跳详情)
|
||||
- **柱状图**:各测试预填充(空心柱)vs 解码(实心柱)速度对比
|
||||
@@ -106,6 +108,7 @@ pip install -r requirements.txt
|
||||
| POST | `/api/configs/test` | 测试连接 |
|
||||
| GET/POST | `/api/tests?page=&page_size=&q=&status=&provider=&sort=&order=` | 测试历史(分页/搜索/筛选/排序)/ 启动测试(异步) |
|
||||
| DELETE | `/api/tests` | 一键清空全部测试历史(含采样与日志) |
|
||||
| GET | `/api/tests/export` | 一键导出全部历史记录(JSON 文件下载,API Key 打码) |
|
||||
| GET | `/api/tests/<id>` | 测试详情(含 runs / logs / summary / 按长度分组) |
|
||||
| GET | `/api/tests/<id>/logs?after=<id>` | 增量日志(前端轮询用) |
|
||||
| POST | `/api/tests/<id>/cancel` | 停止测试 |
|
||||
@@ -183,4 +186,4 @@ llm-speed-tester/
|
||||
## Git
|
||||
|
||||
- **仓库:** `hz4th_coder/llm-speed-tester`
|
||||
- **版本:** v2.6.0(历史分页+筛选+搜索+表头排序;一键清空历史;右下角置顶/置底悬浮按钮)
|
||||
- **版本:** v2.7.0(一键导出全部历史(JSON);左侧三块配置卡片可折叠展开(状态持久化);置顶/置底按钮高亮蓝渐变配色)
|
||||
@@ -3,6 +3,7 @@
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from flask import Flask, jsonify, request, send_file, send_from_directory
|
||||
@@ -146,6 +147,24 @@ def start_test():
|
||||
return jsonify({"ok": True, "id": tid})
|
||||
|
||||
|
||||
@app.route("/api/tests/export")
|
||||
def export_tests():
|
||||
"""一键导出全部历史记录(JSON 文件下载,API Key 打码)"""
|
||||
data = db.export_tests()
|
||||
payload = {
|
||||
"ok": True,
|
||||
"count": len(data),
|
||||
"exported_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"tests": data,
|
||||
}
|
||||
resp = app.response_class(json.dumps(payload, ensure_ascii=False),
|
||||
mimetype="application/json")
|
||||
resp.headers["Content-Disposition"] = (
|
||||
'attachment; filename="llm_speed_tests_%s.json"'
|
||||
% datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/api/tests", methods=["GET"])
|
||||
def list_tests():
|
||||
"""历史记录列表:支持关键词搜索 q、状态 status(逗号多选)、提供商 provider、
|
||||
|
||||
+24
@@ -301,6 +301,30 @@ def delete_test(tid: int):
|
||||
conn.close()
|
||||
|
||||
|
||||
def export_tests():
|
||||
"""导出全部历史:每条含 基本字段 + 配置(Key打码) + 生成参数 + 摘要指标,
|
||||
不含逐条采样与完整日志(避免文件过大)"""
|
||||
with _lock:
|
||||
conn = _connect()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT id,created_at,status,provider,model,name,config_json,gen_cfg_json,summary_json,error "
|
||||
"FROM tests ORDER BY id").fetchall()
|
||||
out = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
cfg = json.loads(d.pop("config_json") or "{}")
|
||||
if cfg.get("api_key"):
|
||||
cfg["api_key"] = "******"
|
||||
d["config"] = cfg
|
||||
d["gen"] = json.loads(d.pop("gen_cfg_json") or "{}")
|
||||
d["summary"] = json.loads(d.pop("summary_json") or "{}")
|
||||
out.append(d)
|
||||
return out
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ───────────────────────── 采样指标 ─────────────────────────
|
||||
|
||||
def add_run(tid: int, run_index: int, metrics: dict, error: str = "", context_length: int = 0):
|
||||
|
||||
+36
-7
@@ -43,17 +43,44 @@ body {
|
||||
|
||||
/* ── 布局 ── */
|
||||
.layout { display: grid; grid-template-columns: 340px 1fr; gap: 16px; padding: 16px 24px; align-items: start; }
|
||||
.config-panel { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 72px; }
|
||||
.config-panel {
|
||||
display: flex; flex-direction: column; gap: 16px;
|
||||
position: sticky; top: 72px;
|
||||
/* 左侧独立滚动:面板固定视口高度,鼠标在左侧滚动时只翻左侧几个块,不带动右侧 */
|
||||
max-height: calc(100vh - 88px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: 6px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
.config-panel::-webkit-scrollbar { width: 8px; }
|
||||
.config-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
.config-panel::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
||||
.main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||
|
||||
.card {
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 16px;
|
||||
}
|
||||
/* ── 可折叠卡片(左侧三块) ── */
|
||||
.card h2 { font-size: 15px; margin-bottom: 14px; }
|
||||
.collapse-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
cursor: pointer; user-select: none;
|
||||
}
|
||||
.collapse-head:hover { color: var(--accent); }
|
||||
.collapse-arrow {
|
||||
font-size: 12px; color: var(--muted); transition: transform .15s;
|
||||
}
|
||||
.card.collapsed .collapse-arrow { transform: rotate(-90deg); }
|
||||
.card.collapsed .collapse-body { display: none; }
|
||||
|
||||
/* ── 卡片头部 ── */
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||
.card-head h2 { margin-bottom: 0; }
|
||||
|
||||
|
||||
/* ── 表单 ── */
|
||||
.field { margin-bottom: 12px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 5px; }
|
||||
@@ -279,20 +306,22 @@ pre.json-box {
|
||||
padding: 10px; font: 11.5px/1.6 var(--mono); overflow: auto; max-height: 260px; color: #a8f0d0;
|
||||
}
|
||||
|
||||
/* ── 右下角一键置顶/置底 ── */
|
||||
/* ── 右下角一键置顶/置底(高对比配色,易于发现) ── */
|
||||
.scroll-fab {
|
||||
position: fixed; right: 20px; bottom: 20px;
|
||||
display: flex; flex-direction: column; gap: 8px; z-index: 60;
|
||||
}
|
||||
.fab {
|
||||
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--border);
|
||||
background: var(--panel2); color: var(--text); font-size: 16px; cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,.45); transition: .15s; line-height: 1;
|
||||
width: 44px; height: 44px; border-radius: 50%; border: none;
|
||||
background: linear-gradient(135deg, #5a92ff, #2f6bff);
|
||||
color: #fff; font-size: 18px; cursor: pointer;
|
||||
box-shadow: 0 6px 18px rgba(47,107,255,.55); transition: .15s; line-height: 1;
|
||||
}
|
||||
.fab:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.fab:hover { filter: brightness(1.15); transform: translateY(-1px); }
|
||||
.fab:active { transform: translateY(0); }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.config-panel { position: static; }
|
||||
.config-panel { position: static; max-height: none; overflow: visible; }
|
||||
.metrics { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
+13
-6
@@ -19,8 +19,9 @@
|
||||
<main class="layout">
|
||||
<!-- 左侧:配置 -->
|
||||
<aside class="config-panel">
|
||||
<section class="card">
|
||||
<h2>🔌 大模型接口配置</h2>
|
||||
<section class="card collapsible" id="sec-interface">
|
||||
<h2 class="collapse-head">🔌 大模型接口配置<span class="collapse-arrow">▾</span></h2>
|
||||
<div class="collapse-body">
|
||||
<div class="field">
|
||||
<label>提供商类型</label>
|
||||
<select id="cfg-provider">
|
||||
@@ -75,10 +76,12 @@
|
||||
</div>
|
||||
<button class="btn block" id="btn-test-conn">🔍 测试连接</button>
|
||||
<div class="conn-result" id="conn-result" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>🚀 速度测试配置</h2>
|
||||
<section class="card collapsible" id="sec-test">
|
||||
<h2 class="collapse-head">🚀 速度测试配置<span class="collapse-arrow">▾</span></h2>
|
||||
<div class="collapse-body">
|
||||
<div class="field">
|
||||
<label>测试名称(主题)</label>
|
||||
<input id="gen-name" type="text" placeholder="如:DeepSeek-V4 不同上下文长度速度对比">
|
||||
@@ -126,9 +129,11 @@
|
||||
<button class="btn primary block" id="btn-start">▶ 开始测试</button>
|
||||
<button class="btn danger block" id="btn-cancel" disabled>■ 停止</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>📤 评测站同步</h2>
|
||||
<section class="card collapsible" id="sec-eval">
|
||||
<h2 class="collapse-head">📤 评测站同步<span class="collapse-arrow">▾</span></h2>
|
||||
<div class="collapse-body">
|
||||
<div class="field">
|
||||
<label>评测站地址</label>
|
||||
<div class="icon-input"><span class="icon">🌐</span><input id="eval-url" placeholder="http://127.0.0.1:16066"></div>
|
||||
@@ -147,6 +152,7 @@
|
||||
<button class="btn block" id="btn-eval-test">🔗 测试连接</button>
|
||||
<div class="conn-result" id="eval-result" hidden></div>
|
||||
<div class="hint" style="margin-top:6px">测试完成后,在历史列表或详情里点「📤 发送到评测站」,一键把结果发布到模型评测网站(端口 16066)对应账号下。</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
@@ -179,6 +185,7 @@
|
||||
<div class="log-actions">
|
||||
<span class="hint" id="hist-selected"></span>
|
||||
<button class="btn small primary" id="btn-compare">⚖️ 对比所选</button>
|
||||
<button class="btn small" id="btn-export-history" title="一键导出全部历史记录(JSON)">📥 导出全部</button>
|
||||
<button class="btn small danger" id="btn-clear-history" title="一键清空全部历史记录(不可恢复)">🗑 清空历史</button>
|
||||
<button class="btn small" id="btn-refresh-history">刷新</button>
|
||||
</div>
|
||||
|
||||
@@ -1098,8 +1098,23 @@ function bind() {
|
||||
$("#btn-export-log").addEventListener("click", exportCurrentLog);
|
||||
$("#btn-refresh-history").addEventListener("click", () => { histState.page = 1; loadHistory(); });
|
||||
$("#btn-compare").addEventListener("click", openCompare);
|
||||
$("#btn-export-history").addEventListener("click", exportAllHistory);
|
||||
$("#btn-clear-history").addEventListener("click", clearAllHistory);
|
||||
|
||||
// 左侧三块:点击标题折叠/展开(状态存 localStorage)
|
||||
const COLLAPSE_KEY = "llmst.collapsed";
|
||||
let collapsedSections = new Set();
|
||||
try { collapsedSections = new Set(JSON.parse(localStorage.getItem(COLLAPSE_KEY) || "[]")); } catch (e) {}
|
||||
document.querySelectorAll(".card.collapsible").forEach((sec) => {
|
||||
if (collapsedSections.has(sec.id)) sec.classList.add("collapsed");
|
||||
const head = sec.querySelector(".collapse-head");
|
||||
if (head) head.addEventListener("click", () => {
|
||||
const on = sec.classList.toggle("collapsed");
|
||||
if (on) collapsedSections.add(sec.id); else collapsedSections.delete(sec.id);
|
||||
try { localStorage.setItem(COLLAPSE_KEY, JSON.stringify([...collapsedSections])); } catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
// 历史:筛选 / 搜索 / 每页条数 / 分页 / 重置
|
||||
let histQTimer = null;
|
||||
$("#hist-q").addEventListener("input", () => {
|
||||
@@ -1223,6 +1238,25 @@ async function clearAllHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportAllHistory() {
|
||||
try {
|
||||
const resp = await fetch("/api/tests/export");
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
const blob = await resp.blob();
|
||||
const cd = resp.headers.get("Content-Disposition") || "";
|
||||
const m = cd.match(/filename\"?=?\"?([^";]+)\"?/);
|
||||
const name = m ? m[1] : "llm_speed_tests.json";
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = name;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
toast("✅ 已导出全部历史(共 " + (blob.size / 1024).toFixed(1) + " KB)");
|
||||
} catch (e) {
|
||||
toast("❌ 导出失败:" + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/* ───────────────────────── 初始化 ───────────────────────── */
|
||||
|
||||
(async function init() {
|
||||
|
||||
Reference in New Issue
Block a user