Compare commits

...

2 Commits

Author SHA1 Message Date
caoqianming dadcc8e7e7 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>
2026-07-13 09:27:40 +08:00
caoqianming 073d9ef025 feat(prompt): look_at_image 引导改"慢、谨慎调"口径(bump 0.58.13)
tool description 与 system prompt 媒体段原本强调"便宜(< ¥0.01)",等于鼓励随手调;
实际痛点是慢(一次几十秒)。两处改为:SLOW / 谨慎调用 / 只在确实需要图内容时调 /
想问的一次在 question 里问全 / 绝不对同一张图反复看;"何时不调"补"图内容可有可无"。
纯 prompt 调整,无行为代码变化。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 09:23:42 +08:00
7 changed files with 58 additions and 17 deletions

View File

@ -23,6 +23,8 @@
### 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.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-12 / 0.58.10**:**mermaid 渲染真·终案:init.sh loopback DROP 掐死 puppeteer↔chromium**(task 9dcae061 四轮收官):0.58.8 部署后(镜像已 chromium .100、全新容器)复测仍恒定 ~2m15s 超时 + `[object Object]` + user CPU 近 0 → 复刻 pool 全款硬限制容器 bisect 定位:**init.sh `127.0.0.0/8 DROP` 挡掉 127.0.0.1 上的 DevTools WebSocket**(puppeteer 连 chromium 的控制通道),SYN DROP → TCP 重试打满 ~127s;容器内放行 `-o lo` 后小图 3.9s、真实大图原参数(`-w 2800 -s 2`)5.2s 出图——"1 vCPU 渲大图慢"亦为误判。**90 天 67 次 timeout 真凶是它**(chromium 坏版本只解释 26 次 launch fail);之前所有 probe 因 `--entrypoint bash` 绕过 init.sh 无 iptables,全绿误判三轮。修复:init.sh `apply_blocklist` 首条 `iptables -A OUTPUT -o lo -j ACCEPT`(v6 同款),原 127.0.0.11:53 DNS 豁免收编,`127.0.0.0/8`/`::1` DROP 降为 belt-and-suspenders 并修正"防回头打宿主"错误注释(容器 127.0.0.1 是自己 netns 的 lo,到不了宿主)。DESIGN §7.5 #1 红线段同步语义修正;RUN.md 故障兜底加终案条目(含 probe 大坑);probe_mermaid.sh 头部加 entrypoint-bypass 警示;CHANGELOG 面向用户补记。生效条件:镜像重建(init.sh 是 COPY 层,秒过)+ 回收存量容器。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.58.12"
__version__ = "0.58.14"

View File

@ -75,9 +75,9 @@ from core.bocha_client import BochaConfig
# look_at_image / seedance 跟 ARK_API_KEY 走;图像段按本 run 选中的 image variant
# 二选一(seedream=豆包 / gpt_image=unifyllm 网关),同一 run 只挂一个图像工具。
_MEDIA_LOOK_SEG = """\
- `look_at_image` 看图 / 读图(豆包 Seed 2.0 Lite 视觉)**(主模型)是纯文本看不见图,""图就调它**:OCR 文字描述画面读图表/表格/示意图识别物体每次很便宜( token,通常 < ¥0.01)
- **何时调**:用户消息里出现 `[用户上传的参考图] <路径>` 且需要据图内容回答("这图里写了啥 / 是什么 / 表格数据多少");或要基于 task 内某张图(`figures/xxx.png`)**实际内容**做事(不是改图,改图走 seedream) `image=<路径>` + 可选 `question`
- **何时不调**:用户只是要改图( seedream i2i)/ 只要文件名不关心内容 / 图是你自己刚生成的且 prompt 已知(无需再读)别对同一张图无意义反复看(每次都烧 token)"""
- `look_at_image` 看图 / 读图(豆包 Seed 2.0 Lite 视觉)**(主模型)是纯文本看不见图,""图就调它**:OCR 文字描述画面读图表/表格/示意图识别物体**很慢(一次要几十秒),谨慎调用** 只在确实需要图的实际内容才调
- **何时调**:用户消息里出现 `[用户上传的参考图] <路径>` 且需要据图内容回答("这图里写了啥 / 是什么 / 表格数据多少");或要基于 task 内某张图(`figures/xxx.png`)**实际内容**做事(不是改图,改图走 seedream) `image=<路径>` + 可选 `question`,**把想知道的一次在 `question` 里问全**,别分多次看
- **何时不调**:用户只是要改图( seedream i2i)/ 只要文件名不关心内容 / 图是你自己刚生成的且 prompt 已知(无需再读)/ 图的内容对当前任务可有可无**绝不对同一张图反复看**(每看一次都是几十秒等待)"""
_MEDIA_SEEDREAM_SEG = """\
- `seedream` 豆包图像生成 / 改图产物自动落 `<task_dir>/figures/`每次 **¥0.22**(联网 `search=true` ¥0.05)
- **文生图**(不传 `reference_images`):从零按 prompt **改图 i2i**( `reference_images=["figures/xxx.png"]`):在已有图上做像素级修改**用户对刚生成 / 上传的图说"改成 X / 换个颜色 / 去掉某处" 必须走改图(reference_images 指那张图),绝不重新文生图**(重画 = 完全不同的图,丢原构图)v1 改图仅支持单张参考

View File

@ -16,6 +16,7 @@
"""
from __future__ import annotations
import math
import re
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
@ -70,13 +71,20 @@ def scan_tool_failures(
min_count: int = 5,
min_tasks: int = 2,
) -> List[Dict[str, Any]]:
"""扫近 `days` 天的 tool 错误消息,返回超阈值的聚集(count 降序)
"""扫近 `days` 天的 tool 错误消息,返回超阈值的聚集
阈值语义:同签名 >= min_count >= min_tasks task task
模型试错几次就自愈的正常噪音不触发; task 复现的才是平台性问题
时间分布:每个聚集带 `daily`( now 往回按 24h 分桶的次数,,
非日历日) `count_24h`(= daily 尾桶) 修复部署后看尾桶是否归零,
区分还在发生窗口内的存量记录排序: 24h 活跃的在前
(count_24h 降序),其后按 count 降序
同步阻塞(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 = []
with session_scope() as s:
rows = s.execute(
@ -102,6 +110,8 @@ def scan_tool_failures(
if hit is None:
continue
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))
c = agg.get(key)
if c is None:
@ -112,14 +122,21 @@ def scan_tool_failures(
"count": 0,
"tasks": set(),
"users": set(),
"last_at": created_at,
"first_at": ts,
"last_at": ts,
"sample": content[:300],
"daily": [0] * n_buckets,
}
c["count"] += 1
c["tasks"].add(task_id)
c["users"].add(user_id)
if created_at > c["last_at"]:
c["last_at"] = created_at
# 分桶:距 now 每满 24h 退一桶,尾桶 = 近 24h(时钟漂移/边界值 clamp 进首尾桶)
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]
out = []
@ -131,22 +148,28 @@ def scan_tool_failures(
"signature": c["signature"],
"kind": c["kind"],
"count": c["count"],
"count_24h": c["daily"][-1],
"daily": c["daily"],
"task_count": len(c["tasks"]),
"user_count": len(c["users"]),
"first_at": c["first_at"].isoformat(),
"last_at": c["last_at"].isoformat(),
"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
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:
n24 = c.get("count_24h", 0)
lines.append(
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']})"
)
lines.append(f" 签名: {c['signature']}")

View File

@ -37,7 +37,9 @@ class LookAtImageTool(Tool):
"Use to OCR text, describe a picture, read charts/tables/diagrams, or identify objects in an "
"image the user uploaded (look for a `[用户上传的参考图] <path>` line in their message) or that "
"was generated/saved in the task. Pass the image path; optionally a specific question "
"(default: describe + OCR everything). Cheap (token-based, usually < ¥0.01). "
"(default: describe + OCR everything). SLOW (tens of seconds per call) — only call when you "
"genuinely need the image's actual content to proceed, and ask everything you need in ONE "
"call (bundle all questions into `question`); never re-read the same image. "
"Returns the model's textual reading of the image."
)
parameters = {

View File

@ -96,6 +96,9 @@
th:first-child, td:first-child { text-align: left; }
th { color: var(--muted); font-weight: 600; }
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; }
.bar-cell { position: relative; }
tfoot .total-row td { border-top: 2px solid var(--border); border-bottom: none; font-weight: 700; }

View File

@ -290,21 +290,32 @@ function renderStorage(d) {
// 工具失败聚集(近7天,低阈值看全量;巡检邮件走 core/toolfail 高阈值)。
// 签名已在后端归一(路径/数字抹平),悬浮 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) {
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.kind)}</td>`
+ `<td class="email" title="${escapeHtml(c.sample || "")}">${escapeHtml(c.signature)}</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.user_count}</td>`
+ `<td>${c.last_at ? fmtTime(c.last_at) : "—"}</td>`
+ `</tr>`).join("")
|| `<tr><td colspan="7" class="empty">近 ${d.days || 7} 天无失败聚集</td></tr>`;
$("s-toolfail").innerHTML = `<div class="card"><h2>工具失败聚集(近 ${d.days || 7} 天,同签名 ≥3 次</h2>`
|| `<tr><td colspan="9" class="empty">近 ${d.days || 7} 天无失败聚集</td></tr>`;
$("s-toolfail").innerHTML = `<div class="card"><h2>工具失败聚集(近 ${d.days || 7} 天,同签名 ≥3 次;近 24h 活跃在前,灰行 = 已安静</h2>`
+ `<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>`;
}