feat(ops): toolfail 聚集加时间分布,活跃/已安静一眼分(bump 0.58.14)
修掉的问题旧记录挂满 7 天窗口,和还在发生的没视觉区别。聚合加 daily(24h 分桶)/count_24h/first_at,活跃在前安静沉底;admin 表 加近24h+趋势列,安静行淡化;告警邮件带近24h。API 只加字段。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
073d9ef025
commit
dadcc8e7e7
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-07
|
### 2026-07
|
||||||
|
|
||||||
|
- **07-13 / 0.58.14**:**toolfail 聚集加时间分布,活跃/已安静一眼分**(0.58.11 的可观测性补强):此前聚集只有全窗口累计 count+last_at——修掉的问题旧记录仍挂满 7 天窗口,和"还在发生"的没视觉区别,只能人肉盯"最近时间"是否前进。`core/toolfail.py` 聚合时加 `daily`(从 now 往回按 24h 分桶,旧→新,非日历日)/ `count_24h`(=尾桶)/ `first_at`,排序改"近 24h 活跃在前(count_24h 降序),安静的按 count 降序沉底";兼补 naive timestamp 的 tzinfo 兜底。admin 页表格加"近24h"(>0 红)和"趋势"列(unicode block mini 条形图,悬浮看逐桶数值),近24h=0 的行整体淡化;告警邮件每条带"近24h N 次/已安静"。API 只加字段不删,向后兼容。冒烟:伪造行 patch session_scope 全绿(分桶/排序/naive ts/邮件格式)。修复部署后次日看板:对应行变灰+趋势尾格归零 = 修好;仍红 = 没修好。
|
||||||
- **07-13 / 0.58.13**:**look_at_image 引导口径改"慢、谨慎调"**:tool description 原写 "Cheap (usually < ¥0.01)"、system prompt 媒体段写"每次很便宜"——都在鼓励随手调,而实际痛点是慢(一次几十秒)。两处(`tools/look_at_image.py` description + `core/agent_builder.py` `_MEDIA_LOOK_SEG`)改为强调 SLOW / 谨慎调用 / 只在确实需要图内容时调 / 想问的一次在 `question` 里问全、绝不对同一张图反复看;"何时不调"加"图内容对当前任务可有可无"。纯 prompt 调整,无行为代码变化。
|
- **07-13 / 0.58.13**:**look_at_image 引导口径改"慢、谨慎调"**:tool description 原写 "Cheap (usually < ¥0.01)"、system prompt 媒体段写"每次很便宜"——都在鼓励随手调,而实际痛点是慢(一次几十秒)。两处(`tools/look_at_image.py` description + `core/agent_builder.py` `_MEDIA_LOOK_SEG`)改为强调 SLOW / 谨慎调用 / 只在确实需要图内容时调 / 想问的一次在 `question` 里问全、绝不对同一张图反复看;"何时不调"加"图内容对当前任务可有可无"。纯 prompt 调整,无行为代码变化。
|
||||||
- **07-13 / 0.58.12**:**沙箱镜像补 `file` 命令**(toolfail 巡检首批产出):7 天 13 任务 9 用户撞 `bash: file: command not found`——模型爱用 `file` 验产物真实类型,slim 基底不带。Dockerfile 加独立小 apt 层(pip/chromium 大层之后,不打穿 cache)。同批评估过 olefile/cairosvg **决定不装**:量小(各 2-3 任务)、模型容器内 pip install 可自愈(样例实证),且 cairosvg 进共享 requirements 会坑 Windows host(cairo DLL);待巡检趋势说话。
|
- **07-13 / 0.58.12**:**沙箱镜像补 `file` 命令**(toolfail 巡检首批产出):7 天 13 任务 9 用户撞 `bash: file: command not found`——模型爱用 `file` 验产物真实类型,slim 基底不带。Dockerfile 加独立小 apt 层(pip/chromium 大层之后,不打穿 cache)。同批评估过 olefile/cairosvg **决定不装**:量小(各 2-3 任务)、模型容器内 pip install 可自愈(样例实证),且 cairosvg 进共享 requirements 会坑 Windows host(cairo DLL);待巡检趋势说话。
|
||||||
- **07-13 / 0.58.11**:**工具失败聚集巡检**(反 9dcae061 "挂 90 天无人知"的结构性缺口):新模块 `core/toolfail.py`——扫近 7 天 messages 里 role=tool 的错误结果(`[Error` 开头 / `command timed out` / 尾部 `[exit 非0]`),按 (工具名+归一化签名:路径/数字/uuid 抹平) 聚合,同签名 ≥5 次且跨 ≥2 task 判聚集(单 task 内试错自愈不触发)。三个出口:① web/app.py `_toolfail_scanner` 周期任务(仿 _disk_scanner,每天跑,签名进程内去重)→ 发 `ZCBOT_DEVELOPER_EMAIL`(复用 SMTP_*,未配则日志);② `GET /v1/admin/tool-failures`;③ admin 页新"工具失败"表(低阈值 3 次/1 task 看全量,悬浮看样例)。生产 DB 冒烟:一次扫出 21 类真聚集(沙箱缺 `file` 命令 x13 跨 13 task、web_fetch Network unreachable x7、mmdc 超时批次等)。env:`ZCBOT_DEVELOPER_EMAIL` / `ZCBOT_TOOLFAIL_SCAN_INTERVAL`(RUN.md 环境段已记)。无新表无 migration;重启后同签名重推一次(聚集还在=问题还在,可接受)。
|
- **07-13 / 0.58.11**:**工具失败聚集巡检**(反 9dcae061 "挂 90 天无人知"的结构性缺口):新模块 `core/toolfail.py`——扫近 7 天 messages 里 role=tool 的错误结果(`[Error` 开头 / `command timed out` / 尾部 `[exit 非0]`),按 (工具名+归一化签名:路径/数字/uuid 抹平) 聚合,同签名 ≥5 次且跨 ≥2 task 判聚集(单 task 内试错自愈不触发)。三个出口:① web/app.py `_toolfail_scanner` 周期任务(仿 _disk_scanner,每天跑,签名进程内去重)→ 发 `ZCBOT_DEVELOPER_EMAIL`(复用 SMTP_*,未配则日志);② `GET /v1/admin/tool-failures`;③ admin 页新"工具失败"表(低阈值 3 次/1 task 看全量,悬浮看样例)。生产 DB 冒烟:一次扫出 21 类真聚集(沙箱缺 `file` 命令 x13 跨 13 task、web_fetch Network unreachable x7、mmdc 超时批次等)。env:`ZCBOT_DEVELOPER_EMAIL` / `ZCBOT_TOOLFAIL_SCAN_INTERVAL`(RUN.md 环境段已记)。无新表无 migration;重启后同签名重推一次(聚集还在=问题还在,可接受)。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.58.13"
|
__version__ = "0.58.14"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
@ -70,13 +71,20 @@ def scan_tool_failures(
|
||||||
min_count: int = 5,
|
min_count: int = 5,
|
||||||
min_tasks: int = 2,
|
min_tasks: int = 2,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""扫近 `days` 天的 tool 错误消息,返回超阈值的聚集(count 降序)。
|
"""扫近 `days` 天的 tool 错误消息,返回超阈值的聚集。
|
||||||
|
|
||||||
阈值语义:同签名 >= min_count 次 且 跨 >= min_tasks 个 task —— 单 task 内
|
阈值语义:同签名 >= min_count 次 且 跨 >= min_tasks 个 task —— 单 task 内
|
||||||
模型试错几次就自愈的正常噪音不触发;跨 task 复现的才是平台性问题。
|
模型试错几次就自愈的正常噪音不触发;跨 task 复现的才是平台性问题。
|
||||||
|
|
||||||
|
时间分布:每个聚集带 `daily`(从 now 往回按 24h 分桶的次数,旧→新,
|
||||||
|
非日历日)和 `count_24h`(= daily 尾桶)—— 修复部署后看尾桶是否归零,
|
||||||
|
区分「还在发生」和「窗口内的存量记录」。排序:近 24h 活跃的在前
|
||||||
|
(count_24h 降序),其后按 count 降序。
|
||||||
同步阻塞(DB 查询),asyncio 调用方放 to_thread/executor。
|
同步阻塞(DB 查询),asyncio 调用方放 to_thread/executor。
|
||||||
"""
|
"""
|
||||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
now = datetime.now(timezone.utc)
|
||||||
|
cutoff = now - timedelta(days=days)
|
||||||
|
n_buckets = max(1, math.ceil(days))
|
||||||
rows = []
|
rows = []
|
||||||
with session_scope() as s:
|
with session_scope() as s:
|
||||||
rows = s.execute(
|
rows = s.execute(
|
||||||
|
|
@ -102,6 +110,8 @@ def scan_tool_failures(
|
||||||
if hit is None:
|
if hit is None:
|
||||||
continue
|
continue
|
||||||
kind, sig_line = hit
|
kind, sig_line = hit
|
||||||
|
# DB 列若是 naive timestamp(存 UTC),补 tzinfo 才能和 now 做减法
|
||||||
|
ts = created_at if created_at.tzinfo else created_at.replace(tzinfo=timezone.utc)
|
||||||
key = (tool_name or "?", _normalize(sig_line))
|
key = (tool_name or "?", _normalize(sig_line))
|
||||||
c = agg.get(key)
|
c = agg.get(key)
|
||||||
if c is None:
|
if c is None:
|
||||||
|
|
@ -112,14 +122,21 @@ def scan_tool_failures(
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"tasks": set(),
|
"tasks": set(),
|
||||||
"users": set(),
|
"users": set(),
|
||||||
"last_at": created_at,
|
"first_at": ts,
|
||||||
|
"last_at": ts,
|
||||||
"sample": content[:300],
|
"sample": content[:300],
|
||||||
|
"daily": [0] * n_buckets,
|
||||||
}
|
}
|
||||||
c["count"] += 1
|
c["count"] += 1
|
||||||
c["tasks"].add(task_id)
|
c["tasks"].add(task_id)
|
||||||
c["users"].add(user_id)
|
c["users"].add(user_id)
|
||||||
if created_at > c["last_at"]:
|
# 分桶:距 now 每满 24h 退一桶,尾桶 = 近 24h(时钟漂移/边界值 clamp 进首尾桶)
|
||||||
c["last_at"] = created_at
|
age_days = int((now - ts).total_seconds() // 86400)
|
||||||
|
c["daily"][n_buckets - 1 - min(n_buckets - 1, max(0, age_days))] += 1
|
||||||
|
if ts < c["first_at"]:
|
||||||
|
c["first_at"] = ts
|
||||||
|
if ts > c["last_at"]:
|
||||||
|
c["last_at"] = ts
|
||||||
c["sample"] = content[:300]
|
c["sample"] = content[:300]
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
|
|
@ -131,22 +148,28 @@ def scan_tool_failures(
|
||||||
"signature": c["signature"],
|
"signature": c["signature"],
|
||||||
"kind": c["kind"],
|
"kind": c["kind"],
|
||||||
"count": c["count"],
|
"count": c["count"],
|
||||||
|
"count_24h": c["daily"][-1],
|
||||||
|
"daily": c["daily"],
|
||||||
"task_count": len(c["tasks"]),
|
"task_count": len(c["tasks"]),
|
||||||
"user_count": len(c["users"]),
|
"user_count": len(c["users"]),
|
||||||
|
"first_at": c["first_at"].isoformat(),
|
||||||
"last_at": c["last_at"].isoformat(),
|
"last_at": c["last_at"].isoformat(),
|
||||||
"sample": c["sample"],
|
"sample": c["sample"],
|
||||||
})
|
})
|
||||||
out.sort(key=lambda x: x["count"], reverse=True)
|
# 活跃的(近 24h 还在发生)排前面,已安静的沉底 —— 面板/邮件都先看还在烧的
|
||||||
|
out.sort(key=lambda x: (x["count_24h"], x["count"]), reverse=True)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def format_alert(clusters: List[Dict[str, Any]], days: float) -> str:
|
def format_alert(clusters: List[Dict[str, Any]], days: float) -> str:
|
||||||
"""聚集列表 → 告警邮件正文(纯文本)。"""
|
"""聚集列表 → 告警邮件正文(纯文本)。"""
|
||||||
lines = [f"近 {days:g} 天内检出 {len(clusters)} 类工具失败聚集:", ""]
|
lines = [f"近 {days:g} 天内检出 {len(clusters)} 类工具失败聚集(近 24h 活跃的在前):", ""]
|
||||||
for c in clusters:
|
for c in clusters:
|
||||||
|
n24 = c.get("count_24h", 0)
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- [{c['tool']}/{c['kind']}] x{c['count']}"
|
f"- [{c['tool']}/{c['kind']}] x{c['count']}"
|
||||||
f"(task {c['task_count']} 个 / 用户 {c['user_count']} 人,"
|
f"(近24h {n24} 次{',已安静' if not n24 else ''},"
|
||||||
|
f"task {c['task_count']} 个 / 用户 {c['user_count']} 人,"
|
||||||
f"最近 {c['last_at']})"
|
f"最近 {c['last_at']})"
|
||||||
)
|
)
|
||||||
lines.append(f" 签名: {c['signature']}")
|
lines.append(f" 签名: {c['signature']}")
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,9 @@
|
||||||
th:first-child, td:first-child { text-align: left; }
|
th:first-child, td:first-child { text-align: left; }
|
||||||
th { color: var(--muted); font-weight: 600; }
|
th { color: var(--muted); font-weight: 600; }
|
||||||
td.num { font-family: var(--mono); }
|
td.num { font-family: var(--mono); }
|
||||||
|
td.num.hot { color: var(--danger); font-weight: 700; }
|
||||||
|
td.trend { font-family: var(--mono); letter-spacing: 1px; color: var(--accent); }
|
||||||
|
tr.quiet td { opacity: .45; }
|
||||||
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; }
|
tfoot .total-row td { border-top: 2px solid var(--border); border-bottom: none; font-weight: 700; }
|
||||||
|
|
|
||||||
|
|
@ -290,21 +290,32 @@ function renderStorage(d) {
|
||||||
|
|
||||||
// 工具失败聚集(近7天,低阈值看全量;巡检邮件走 core/toolfail 高阈值)。
|
// 工具失败聚集(近7天,低阈值看全量;巡检邮件走 core/toolfail 高阈值)。
|
||||||
// 签名已在后端归一(路径/数字抹平),悬浮 title 给最近一次的原始样例。
|
// 签名已在后端归一(路径/数字抹平),悬浮 title 给最近一次的原始样例。
|
||||||
|
// 后端已按活跃度排序(近24h 有发生的在前);近24h=0 的行淡化 —— 修复部署后
|
||||||
|
// 看对应行是否变灰 + 趋势尾格归零,即知修没修好(旧记录会挂满窗口,行不消失)。
|
||||||
|
function trendBar(daily) {
|
||||||
|
if (!daily || !daily.length) return "";
|
||||||
|
const max = Math.max(...daily, 1);
|
||||||
|
const blocks = "▁▂▃▄▅▆▇█";
|
||||||
|
return daily.map(v => v === 0 ? "·"
|
||||||
|
: blocks[Math.min(blocks.length - 1, Math.ceil(v / max * blocks.length) - 1)]).join("");
|
||||||
|
}
|
||||||
function renderToolFailures(d) {
|
function renderToolFailures(d) {
|
||||||
const rows = d.clusters || [];
|
const rows = d.clusters || [];
|
||||||
const body = rows.map(c => `<tr>`
|
const body = rows.map(c => `<tr${c.count_24h ? "" : ` class="quiet"`}>`
|
||||||
+ `<td>${escapeHtml(c.tool)}</td>`
|
+ `<td>${escapeHtml(c.tool)}</td>`
|
||||||
+ `<td>${escapeHtml(c.kind)}</td>`
|
+ `<td>${escapeHtml(c.kind)}</td>`
|
||||||
+ `<td class="email" title="${escapeHtml(c.sample || "")}">${escapeHtml(c.signature)}</td>`
|
+ `<td class="email" title="${escapeHtml(c.sample || "")}">${escapeHtml(c.signature)}</td>`
|
||||||
+ `<td class="num">${c.count}</td>`
|
+ `<td class="num">${c.count}</td>`
|
||||||
|
+ `<td class="num${c.count_24h ? " hot" : ""}">${c.count_24h || 0}</td>`
|
||||||
|
+ `<td class="trend" title="每格 24h,旧 → 新:${(c.daily || []).join(" / ")}">${trendBar(c.daily)}</td>`
|
||||||
+ `<td class="num">${c.task_count}</td>`
|
+ `<td class="num">${c.task_count}</td>`
|
||||||
+ `<td class="num">${c.user_count}</td>`
|
+ `<td class="num">${c.user_count}</td>`
|
||||||
+ `<td>${c.last_at ? fmtTime(c.last_at) : "—"}</td>`
|
+ `<td>${c.last_at ? fmtTime(c.last_at) : "—"}</td>`
|
||||||
+ `</tr>`).join("")
|
+ `</tr>`).join("")
|
||||||
|| `<tr><td colspan="7" class="empty">近 ${d.days || 7} 天无失败聚集</td></tr>`;
|
|| `<tr><td colspan="9" class="empty">近 ${d.days || 7} 天无失败聚集</td></tr>`;
|
||||||
$("s-toolfail").innerHTML = `<div class="card"><h2>工具失败聚集(近 ${d.days || 7} 天,同签名 ≥3 次)</h2>`
|
$("s-toolfail").innerHTML = `<div class="card"><h2>工具失败聚集(近 ${d.days || 7} 天,同签名 ≥3 次;近 24h 活跃在前,灰行 = 已安静)</h2>`
|
||||||
+ `<div class="scroll-x"><table>`
|
+ `<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>近24h</th><th>趋势</th><th>任务数</th><th>用户数</th><th>最近</th></tr></thead>`
|
||||||
+ `<tbody>${body}</tbody></table></div></div>`;
|
+ `<tbody>${body}</tbody></table></div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue