Files
dsh_shenxian/dsh-server-docs/skills/dsh-auto-handoff-chain/scripts/chain_report.py
T
admin bc0dd2c96d docs(dsh-server-docs): mksess 口径校正(DB 直插→PG 直插)+ 技能 dsh-auto-handoff-chain 三处同步建立(序⑰ S1/S2)
- 02-运维手册.md:mksess 命中处补「PG 直插 + 连接串来源」+ D1 勘误指针两行(档案 77 的旧形态说明,正文不改)
- skills/dsh-change-workflow/SKILL.md(146/395/495 三行)、skills/dsh-env-bootstrap/references/常驻规则-快照.md(55 行):同口径校正
- skills/dsh-auto-handoff-chain/:新建文档库副本(SKILL.md v1.3.2 + scripts/chain_report.py),此前该技能三处同步从未建立
- README.md / INDEX.md:登记该技能(技能表 + 「什么时候查什么」+ 技能清单表)

判据:E1 = 0 行、E2 两副本 md5 全同、E3 diff -r = 0 行、E4 登记命中 README 1 / INDEX 2、镜像 47 同值
2026-09-17 17:03:34 +08:00

234 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""接力链体检 —— 一条命令出「某条接力链跑了哪些棒 / 每棒成本 / 有没有断链」。
用法:
python chain_report.py # 默认看全部会话
python chain_report.py "覆盖网络" # 只看标题含该关键词的会话
python chain_report.py "覆盖网络" --json # 追加输出机器可读 JSON
数据源:
<HOME>/workbuddy.db → sessions / automations / automation_runs
<HOME>/projects/*/<sid>.jsonl → 逐次 rawUsage(含 credit)+ function_call 计数
坑(踩过的,别再踩):
* rawUsage 不在顶层,必须【递归】收集;顶层 d.get("rawUsage") 抓不到。
* 文本元素类型是 input_text / output_text,不是 "text"。
* 工作区目录名会随工作区搬迁改变(e-... / d-...),两个目录都要找。
* 本机没有 sqlite3 CLI ⇒ 必须用 python 的 sqlite3,以 mode=ro 只读打开。
* 读文件必须显式 UTF-8(newline=""),否则 CP936 静默乱码。
"""
import sys, os, io, json, glob, sqlite3, datetime, argparse
sys.stdout.reconfigure(encoding="utf-8")
HOME = os.environ.get("WORKBUDDY_CONFIG_DIR") or os.path.expanduser("~/.workbuddy")
DB = os.path.join(HOME, "workbuddy.db")
def ts(v):
if not v:
return "-"
try:
return datetime.datetime.fromtimestamp(int(v) / 1000).strftime("%m-%d %H:%M")
except Exception:
return str(v)
def dur(a, b):
if not a or not b:
return "-"
try:
return "%dmin" % round((int(b) - int(a)) / 60000.0)
except Exception:
return "-"
def walk_usage(o, hits):
"""递归收集 rawUsage 节点(顶层抓不到)。"""
if isinstance(o, dict):
for k, v in o.items():
if k == "rawUsage" and isinstance(v, dict):
hits.append(v)
else:
walk_usage(v, hits)
elif isinstance(o, list):
for x in o:
walk_usage(x, hits)
def get_text(d):
"""文本元素类型是 input_text / output_text。"""
c = d.get("content")
if isinstance(c, str):
return c
out = []
if isinstance(c, list):
for x in c:
if isinstance(x, dict) and x.get("type") in ("input_text", "output_text", "text"):
out.append(x.get("text", ""))
return "\n".join(out)
def find_jsonl(sid, projdirs):
for d in projdirs:
p = os.path.join(d, sid + ".jsonl")
if os.path.exists(p):
return p
return None
def scan_session(path):
ncall = nuser = nasst = 0
credit = 0.0
tools, skills_missing = {}, 0
with io.open(path, "r", encoding="utf-8", errors="replace", newline="") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
d = json.loads(line)
except Exception:
continue
t = d.get("type")
if t == "function_call":
ncall += 1
nm = d.get("name") or "?"
tools[nm] = tools.get(nm, 0) + 1
elif t == "message":
if d.get("role") == "user":
nuser += 1
elif d.get("role") == "assistant":
nasst += 1
hits = []
walk_usage(d, hits)
for x in hits:
try:
credit += float(x.get("credit") or 0)
except Exception:
pass
return dict(calls=ncall, credit=credit, user=nuser, asst=nasst,
tools=tools, skill=tools.get("Skill", 0))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("keyword", nargs="?", default="")
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
if not os.path.exists(DB):
print("找不到 workbuddy.db: %s" % DB)
return 1
con = sqlite3.connect("file:%s?mode=ro" % DB.replace("?", "%3f"), uri=True)
cur = con.cursor()
kw = args.keyword
sql = "SELECT id,title,created_at,last_activity_at,is_background_automation FROM sessions"
sess = []
for r in cur.execute(sql):
if kw and kw not in (r[1] or ""):
continue
sess.append(r)
sess.sort(key=lambda r: r[2] or 0)
projdirs = glob.glob(os.path.join(HOME, "projects", "*"))
print("=" * 96)
print("接力链体检 · 关键词 = %s · 会话 %d 个 · %s" % (kw or "(全部)", len(sess),
datetime.datetime.now().strftime("%Y-%m-%d %H:%M")))
print("=" * 96)
print("%-10s %-12s %-7s %-5s %-8s %-8s %-7s %-6s %s" % (
"sid", "创建", "时长", "auto", "工具调用", "积分", "积分/次", "Skill", "标题"))
rows = []
T = dict(calls=0, credit=0.0, user=0)
for r in sess:
sid, title = r[0], r[1]
p = find_jsonl(sid, projdirs)
if not p:
print("%-10s %-12s %-7s %-5s %-8s %-8s %-7s %-6s %s" % (
sid[:8], ts(r[2]), dur(r[2], r[3]), r[4], "无转录", "-", "-", "-", title))
continue
s = scan_session(p)
per = (s["credit"] / s["calls"]) if s["calls"] else 0
T["calls"] += s["calls"]
T["credit"] += s["credit"]
T["user"] += s["user"]
print("%-10s %-12s %-7s %-5s %-8d %-8.2f %-7.3f %-6d %s" % (
sid[:8], ts(r[2]), dur(r[2], r[3]), r[4], s["calls"], s["credit"], per,
s["skill"], title))
top = sorted(s["tools"].items(), key=lambda kv: -kv[1])[:5]
if top:
print(" top: " + ", ".join("%s×%d" % (a, b) for a, b in top))
rows.append(dict(sid=sid, title=title, created_at=r[2], **{
k: s[k] for k in ("calls", "credit", "user", "asst", "skill")}))
print("-" * 96)
print("合计: 工具调用 %d | 积分 %.2f | 用户轮 %d" % (T["calls"], T["credit"], T["user"]))
# ---- 自动化运行链(断链检测) ----
print()
print("=" * 96)
print("自动化运行链(每行 = 一次触发 = 一个新会话)")
print("=" * 96)
anames = {r[0]: r[1] for r in cur.execute("SELECT id,name FROM automations")}
recs = []
for r in cur.execute("SELECT automation_id, runs_json FROM automation_runs"):
try:
runs = json.loads(r[1] or "[]")
except Exception:
runs = []
for x in runs:
recs.append((x.get("startedAt") or 0, anames.get(r[0], r[0]), x))
recs.sort()
prev = None
gaps = []
for st, name, x in recs:
if kw and kw not in (name or ""):
continue
if prev:
gap = (int(st) - int(prev)) / 60000.0
if gap > 90:
gaps.append((ts(prev), ts(st), gap))
prev = st
print("%-19s ok=%-5s %-6s %-14s %s" % (
ts(st), x.get("success"),
dur(st, x.get("finishedAt")), str(x.get("conversationId"))[:12], (name or "")[:40]))
if gaps:
print()
print("⚠️ 疑似断链(相邻两棒间隔 > 90 分钟):")
for a, b, g in gaps:
print(" %s → %s (间隔 %.0f 分钟)" % (a, b, g))
# ---- 完成态核查 ----
print()
print("=" * 96)
print("过期但仍 ACTIVE 的一次性 automation(once 型跑完不自动转完成态 ⇒ 有补跑窗口)")
print("=" * 96)
n = 0
for r in cur.execute("SELECT id,name,status,scheduled_at,deleted_at FROM automations ORDER BY created_at DESC"):
if r[2] != "ACTIVE" or r[4] or not r[3]:
continue
try:
when = datetime.datetime.fromisoformat(r[3])
except Exception:
continue
if when < datetime.datetime.now():
n += 1
print(" %s | %-40s | 触发时刻 %s | deleted_at=%s" % (r[0][:8], (r[1] or "")[:40], r[3], r[4]))
if not n:
print(" (无)")
if args.json:
print()
print(json.dumps(rows, ensure_ascii=False, indent=2))
con.close()
return 0
if __name__ == "__main__":
sys.exit(main())