Compare commits
2 Commits
b4808b0370
...
d235cb7564
| Author | SHA1 | Date |
|---|---|---|
|
|
d235cb7564 | |
|
|
1352f092a3 |
12
PROGRESS.md
12
PROGRESS.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-06-26(定时任务执行历史列表(分页)+ bump 0.29.0)
|
最后更新:2026-06-26(admin 近7天用量表加合计行 + bump 0.31.1)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,6 +21,16 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-06-26 / 消息目录圆点错位再修(点击竞态 + 触底兜底)(bump 0.31.2)
|
||||||
|
|
||||||
|
- 现象(0.20.4 后仍残留):① 点圆点,被点的圆点不变红、活跃态跑到途经轮次(尤其点 #1 跳到 #2);② 点最后一个 / 滚到底,倒数第二个变红。
|
||||||
|
- 根因:① `jumpToMessage` 的 `scrollIntoView({behavior:"smooth"})` 在动画途中连发 scroll 事件,`updateActiveOutlineDot` 按动画途中位置反复改写,抢走刚 `setActiveOutlineIdx` 的显式点选;② 「顶线以上最后一卡」判活跃,最后几轮永远顶不到顶线(容器先到底)→ 永远停在倒数第二个,这是 scroll-spy 经典「不可达末项」bug,普通滚动也复现。
|
||||||
|
- 修复(`web/static/js/chat.js`):① 加 `_outlineJumpLock`,点选后锁定活跃态,平滑滚动期间 `updateActiveOutlineDot` 直接返回,700ms 兜底解锁并按落点重算一次;② `updateActiveOutlineDot` 加触底分支——滚到容器底且无更新内容可加载(`!msgHasMoreNewer`)时,直接判最后一个已加载轮为当前。
|
||||||
|
|
||||||
|
### 2026-06-26 / admin 近7天用量表加合计行(bump 0.31.1)
|
||||||
|
|
||||||
|
- 纯前端展示:`renderByDay`(`web/static/js/admin.js`)在 `by_day_7d` 表底加 `<tfoot>` 合计行,对 7 天 cost_cny/tokens_in/tokens_out 求和;`tfoot .total-row` 样式(粗体 + 上分隔线)在 `admin.html`。无数据时不渲染合计行。后端数据已有(`_usage_section`),无改动。
|
||||||
|
|
||||||
### 2026-06-26 / per-account 模型访问控制(档位制,复用 plan 列)(bump 0.31.0)
|
### 2026-06-26 / per-account 模型访问控制(档位制,复用 plan 列)(bump 0.31.0)
|
||||||
|
|
||||||
- 需求:管理后台按账户控制可调用哪些模型。deepseek flash/pro + seedream/seedance + 内网 local 对所有人开放,doubao/glm 按账户分配。
|
- 需求:管理后台按账户控制可调用哪些模型。deepseek flash/pro + seedream/seedance + 内网 local 对所有人开放,doubao/glm 按账户分配。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.31.0"
|
__version__ = "0.31.2"
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@
|
||||||
td.num { font-family: var(--mono); }
|
td.num { font-family: var(--mono); }
|
||||||
td.email { font-family: var(--mono); max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
|
td.email { font-family: var(--mono); max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.bar-cell { position: relative; }
|
.bar-cell { position: relative; }
|
||||||
|
tfoot .total-row td { border-top: 2px solid var(--border); border-bottom: none; font-weight: 700; }
|
||||||
.scroll-x { overflow-x: auto; }
|
.scroll-x { overflow-x: auto; }
|
||||||
.empty { color: var(--muted); padding: 8px; text-align: center; }
|
.empty { color: var(--muted); padding: 8px; text-align: center; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,21 @@ function renderByDay(rows) {
|
||||||
+ `<td class="num">${fmtTokens(r.tokens_in)}</td>`
|
+ `<td class="num">${fmtTokens(r.tokens_in)}</td>`
|
||||||
+ `<td class="num">${fmtTokens(r.tokens_out)}</td>`
|
+ `<td class="num">${fmtTokens(r.tokens_out)}</td>`
|
||||||
+ `</tr>`).join("") || `<tr><td colspan="4" class="empty">无数据</td></tr>`;
|
+ `</tr>`).join("") || `<tr><td colspan="4" class="empty">无数据</td></tr>`;
|
||||||
|
const sum = rows.reduce((a, r) => {
|
||||||
|
a.cost_cny += r.cost_cny || 0;
|
||||||
|
a.tokens_in += r.tokens_in || 0;
|
||||||
|
a.tokens_out += r.tokens_out || 0;
|
||||||
|
return a;
|
||||||
|
}, { cost_cny: 0, tokens_in: 0, tokens_out: 0 });
|
||||||
|
const foot = rows.length ? `<tfoot><tr class="total-row">`
|
||||||
|
+ `<td>合计</td>`
|
||||||
|
+ `<td class="num">${fmtCNY(sum.cost_cny)}</td>`
|
||||||
|
+ `<td class="num">${fmtTokens(sum.tokens_in)}</td>`
|
||||||
|
+ `<td class="num">${fmtTokens(sum.tokens_out)}</td>`
|
||||||
|
+ `</tr></tfoot>` : "";
|
||||||
return `<div class="card"><h2>近 7 天用量(按天)</h2><div class="scroll-x"><table>`
|
return `<div class="card"><h2>近 7 天用量(按天)</h2><div class="scroll-x"><table>`
|
||||||
+ `<thead><tr><th>日期</th><th>成本</th><th>输入</th><th>输出</th></tr></thead>`
|
+ `<thead><tr><th>日期</th><th>成本</th><th>输入</th><th>输出</th></tr></thead>`
|
||||||
+ `<tbody>${body}</tbody></table></div></div>`;
|
+ `<tbody>${body}</tbody>${foot}</table></div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按模型(时间筛选 + 排序)。d = {range, sort, rows}
|
// 按模型(时间筛选 + 排序)。d = {range, sort, rows}
|
||||||
|
|
|
||||||
|
|
@ -637,10 +637,11 @@ async function jumpToMessage(idx) {
|
||||||
// 顶部对齐(非居中):第一轮上方无内容无法居中、会被钉到顶端,而 updateActiveOutlineDot
|
// 顶部对齐(非居中):第一轮上方无内容无法居中、会被钉到顶端,而 updateActiveOutlineDot
|
||||||
// 按「顶线」判活跃轮 —— 两套锚点必须一致,否则贴顶时活跃圆点会越界到下一轮。
|
// 按「顶线」判活跃轮 —— 两套锚点必须一致,否则贴顶时活跃圆点会越界到下一轮。
|
||||||
// .msg 的 scroll-margin-top 给卡片留一点上方呼吸空间。
|
// .msg 的 scroll-margin-top 给卡片留一点上方呼吸空间。
|
||||||
|
setActiveOutlineIdx(idx);
|
||||||
|
lockOutlineDuringJump(); // 锁住活跃圆点:平滑滚动途中的 scroll 事件不得把活跃态抢到途经轮次
|
||||||
card.scrollIntoView({ behavior: "smooth", block: "start" });
|
card.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
card.classList.add("msg-jump-flash");
|
card.classList.add("msg-jump-flash");
|
||||||
setTimeout(() => card.classList.remove("msg-jump-flash"), 1200);
|
setTimeout(() => card.classList.remove("msg-jump-flash"), 1200);
|
||||||
setActiveOutlineIdx(idx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 顶/底 sentinel 进视口即自动补更早 / 更新 —— 复用 task list 的同款范式。
|
// 顶/底 sentinel 进视口即自动补更早 / 更新 —— 复用 task list 的同款范式。
|
||||||
|
|
@ -699,14 +700,44 @@ function setActiveOutlineIdx(idx) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点圆点跳转期间锁定活跃态:平滑滚动会连发 scroll 事件,若不锁,updateActiveOutlineDot
|
||||||
|
// 会按动画途中的位置反复改写,把刚点中的圆点抢成途经轮次(表现:点 #1 不变红 / 跳到 #2)。
|
||||||
|
let _outlineJumpLock = false;
|
||||||
|
let _outlineJumpTimer = 0;
|
||||||
|
function lockOutlineDuringJump() {
|
||||||
|
_outlineJumpLock = true;
|
||||||
|
clearTimeout(_outlineJumpTimer);
|
||||||
|
// 平滑滚动一般 <500ms;700ms 兜底解锁后按落点重算一次(触底轮由下面 atBottom 分支兜)
|
||||||
|
_outlineJumpTimer = setTimeout(() => {
|
||||||
|
_outlineJumpLock = false;
|
||||||
|
updateActiveOutlineDot();
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
|
||||||
// 视口顶线以上的最后一个已加载 user 卡 = 当前轮,高亮对应圆点
|
// 视口顶线以上的最后一个已加载 user 卡 = 当前轮,高亮对应圆点
|
||||||
function updateActiveOutlineDot() {
|
function updateActiveOutlineDot() {
|
||||||
const rail = $("msg-outline-rail");
|
const rail = $("msg-outline-rail");
|
||||||
if (!rail || rail.style.display === "none") return;
|
if (!rail || rail.style.display === "none") return;
|
||||||
|
if (_outlineJumpLock) return; // 显式跳转动画期间不抢
|
||||||
const wrap = $("chat-stream");
|
const wrap = $("chat-stream");
|
||||||
|
const items = state.outline || [];
|
||||||
|
|
||||||
|
// 触底兜底:最后几轮永远顶不到顶线(容器先到底),按原逻辑会一直停在倒数第二个
|
||||||
|
// (表现:点最后一个 / 滚到底时倒数第二个变红)。滚到容器底且无更新内容可加载时,
|
||||||
|
// 直接判最后一个已加载轮为当前。
|
||||||
|
if (!state.msgHasMoreNewer
|
||||||
|
&& wrap.scrollTop + wrap.clientHeight >= wrap.scrollHeight - 2) {
|
||||||
|
for (let i = items.length - 1; i >= 0; i--) {
|
||||||
|
if (wrap.querySelector(`.msg[data-idx="${items[i].idx}"]`)) {
|
||||||
|
setActiveOutlineIdx(items[i].idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const top = wrap.getBoundingClientRect().top;
|
const top = wrap.getBoundingClientRect().top;
|
||||||
let activeIdx = null;
|
let activeIdx = null;
|
||||||
for (const it of (state.outline || [])) {
|
for (const it of items) {
|
||||||
const card = wrap.querySelector(`.msg[data-idx="${it.idx}"]`);
|
const card = wrap.querySelector(`.msg[data-idx="${it.idx}"]`);
|
||||||
if (!card) continue;
|
if (!card) continue;
|
||||||
// 容差与 .msg 的 scroll-margin-top(16px)对齐:贴顶的短第一轮判到自己,不越界
|
// 容差与 .msg 的 scroll-margin-top(16px)对齐:贴顶的短第一轮判到自己,不越界
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue