feat(admin): 各用户用量表加「最近使用」列(bump 0.34.3)
后端 _user_usage_page 加全量(不随 range 筛选)相关子查询 max(created_at) → last_used_at;前端 renderUserUsage 加列, fmtTimeAgo 显示 + 全时间戳 title,无用量显示「—」。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d8f71aa7b2
commit
eb9ffd654f
|
|
@ -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 硬约束。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.34.2"
|
||||
__version__ = "0.34.3"
|
||||
|
|
|
|||
12
web/admin.py
12
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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
+ `<td class="num">${fmtTokens(r.tokens_out)}</td>`
|
||||
+ `<td class="num">${hitRate}%</td>`
|
||||
+ `<td class="num">${r.n_events || 0}</td>`
|
||||
+ `<td title="${escapeHtml(fmtTime(r.last_used_at))}">${r.last_used_at ? fmtTimeAgo(r.last_used_at) : "—"}</td>`
|
||||
+ `</tr>`;
|
||||
}).join("") || `<tr><td colspan="7" class="empty">无数据</td></tr>`;
|
||||
}).join("") || `<tr><td colspan="8" class="empty">无数据</td></tr>`;
|
||||
$("s-users").innerHTML = `<div class="card">`
|
||||
+ `<div class="card-head"><h2>各用户用量(${rangeLabel(d.range)})</h2>${ctrlHTML("u", d.range, d.sort)}</div>`
|
||||
+ tierLegendHTML()
|
||||
+ `<div class="scroll-x"><table>`
|
||||
+ `<thead><tr><th>用户</th><th>档位</th><th>成本</th><th>输入</th><th>输出</th><th>缓存命中</th><th>事件</th></tr></thead>`
|
||||
+ `<thead><tr><th>用户</th><th>档位</th><th>成本</th><th>输入</th><th>输出</th><th>缓存命中</th><th>事件</th><th>最近使用</th></tr></thead>`
|
||||
+ `<tbody>${body}</tbody></table></div>`
|
||||
+ pagerHTML("uu", page, maxPage, from, to, total)
|
||||
+ `</div>`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue