From eb9ffd654f1e9f3f0e8283821ed32074dfdd3223 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 1 Jul 2026 13:28:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=90=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=94=A8=E9=87=8F=E8=A1=A8=E5=8A=A0=E3=80=8C=E6=9C=80=E8=BF=91?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E3=80=8D=E5=88=97(bump=200.34.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 _user_usage_page 加全量(不随 range 筛选)相关子查询 max(created_at) → last_used_at;前端 renderUserUsage 加列, fmtTimeAgo 显示 + 全时间戳 title,无用量显示「—」。 Co-Authored-By: Claude Opus 4.8 (1M context) --- PROGRESS.md | 3 +++ core/__init__.py | 2 +- web/admin.py | 12 +++++++++++- web/static/js/admin.js | 7 ++++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 0fec2f9..23a2101 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,6 +21,9 @@ ## 已完成关键能力 +### 2026-07-01 / admin 各用户用量加「最近使用」列(bump 0.34.3) +用户需求:admin 页面「各用户用量」表加一列展示每个用户的最近使用时间。改动:`web/admin.py _user_usage_page` 加一个**全量**(不随 range 筛选)的相关子查询 `max(usage_events.created_at)`,新字段 `last_used_at`(ISO 或 null);语义上刻意用全量而非跟着 range 走的 join——否则选 7d/30d 会把更早的真实 last-used 藏掉,列就失去意义。前端 `admin.js renderUserUsage` 加「最近使用」表头 + 单元格,用 `fmtTimeAgo`(相对时间)展示、`fmtTime` 全时间戳作 title 悬浮,无用量用户显示「—」;colspan 7→8。 + ### 2026-07-01 / ppt 页数必须用户显式拍板(bump 0.34.2) 用户反馈:ppt skill 生成时页数总默认到 ~12 张,页数从没被真正确认过。根因是行为层:a–h 八条对齐里 b 项(页数)只给「常 8–15 页」区间,又被打包进整批 BLOCKING 确认,用户一句笼统「OK」就整批过、模型自取区间中位数(~12)。修(纯文档):`SKILL.md` b 项改为推**一个具体数字**+ 标为「独立拍板项」;a–h 表后新增「🔒 页数 gate(不可默认放行)」——用户没给/没显式认可具体张数时必须单独追问「就定 N 页?」拿到明确整数才写逐页大纲,禁止用区间中位数当默认(唯一例外:用户明说「页数你随意」时按推荐数走、仍在预览写出数字供否掉);`strategist.md §b` 同步补 Non-defaultable gate 硬约束。 diff --git a/core/__init__.py b/core/__init__.py index 08b27c2..a7ea280 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.34.2" +__version__ = "0.34.3" diff --git a/web/admin.py b/web/admin.py index d8ccb60..65df07c 100644 --- a/web/admin.py +++ b/web/admin.py @@ -199,6 +199,14 @@ def _user_usage_page(s: Any, page: int, page_size: int, cutoff, sort: str) -> di if cutoff is not None: join_cond = and_(join_cond, UsageEvent.created_at >= cutoff) + # 最近使用时间:取全量(不随 range 筛选变),否则 7d/30d 会把更早的真实 last-used 藏掉。 + last_used_sq = ( + select(func.max(UsageEvent.created_at)) + .where(UsageEvent.user_id == User.user_id) + .correlate(User) + .scalar_subquery() + ) + total_users = s.execute(select(func.count()).select_from(User)).scalar_one() rows = [ { @@ -213,13 +221,15 @@ def _user_usage_page(s: Any, page: int, page_size: int, cutoff, sort: str) -> di "tokens_out": int(to or 0), "tokens_cache_hit": int(h or 0), "n_events": int(n or 0), + "last_used_at": last_used.isoformat() if last_used else None, } - for uid, email, name, uname, role, plan, c, ti, to, h, n in s.execute( + for uid, email, name, uname, role, plan, c, ti, to, h, n, last_used in s.execute( select( User.user_id, User.email, User.name, User.user_name, User.role, User.plan, cost_sum, tin_sum, tout_sum, func.coalesce(func.sum(hit).filter(chat), 0), func.count(UsageEvent.event_id), + last_used_sq.label("last_used_at"), ) .join(UsageEvent, join_cond, isouter=True) .group_by(User.user_id, User.email, User.name, User.user_name, User.role, User.plan) diff --git a/web/static/js/admin.js b/web/static/js/admin.js index a732a4c..e33aa5e 100644 --- a/web/static/js/admin.js +++ b/web/static/js/admin.js @@ -3,7 +3,7 @@ // 结构:左侧目录(点击平滑滚动)+ 右侧内容。overview(固定指标)10s 轮询; // 「按模型」「各用户用量」带时间筛选+排序、「各用户用量」「存储」分页 —— 各自独立 fetch、 // 自管状态(range/sort/page),overview tick 顺手刷新但不丢状态。导出 PDF 走客户端打印。 -import { humanSize, fmtTime, fmtTokens, escapeHtml } from "./format.js"; +import { humanSize, fmtTime, fmtTimeAgo, fmtTokens, escapeHtml } from "./format.js"; const LS_TOKEN = "zcbot.token"; const REFRESH_MS = 10000; @@ -200,13 +200,14 @@ function renderUserUsage(d) { + `${fmtTokens(r.tokens_out)}` + `${hitRate}%` + `${r.n_events || 0}` + + `${r.last_used_at ? fmtTimeAgo(r.last_used_at) : "—"}` + ``; - }).join("") || `无数据`; + }).join("") || `无数据`; $("s-users").innerHTML = `
` + `

各用户用量(${rangeLabel(d.range)})

${ctrlHTML("u", d.range, d.sort)}
` + tierLegendHTML() + `
` - + `` + + `` + `${body}
用户档位成本输入输出缓存命中事件
用户档位成本输入输出缓存命中事件最近使用
` + pagerHTML("uu", page, maxPage, from, to, total) + `
`;