Files
workbuddy_skills/dsh-workflow/references/dsh-auto-handoff-chain/chain_report.py
T
admin e03465c398 按用户令提交:把此前未纳管的 9 个技能目录一并入库
用户令逐字:「E:/ProgramData/.workbuddy/skills  提交仓库是指的这里」——
即本目录就是仓库(2026-10-07 已在本目录建仓,见当日日志 §22),本轮把余下未纳管的 9 个技能一并提交。

本次入库(9 个技能,46 个文件):
1、`AI HOT`
2、`draw-ui`
3、`dsh-diagnose`
4、`dsh-knowledge`
5、`dsh-local-env`
6、`dsh-opensource-release`
7、`dsh-workflow`
8、`oil-motion`
9、`skills-security-check`

提交前核对:
· **凭据类扫描**(`*.env` / `*token*` / `*.key` / `*secret*` / `*.pem`)⇒ **零命中** ✓;
· 体积合计约 20 MB(`draw-ui` 12M + `oil-motion` 6.5M 是大头,形态为配图/素材 ——
  仓库 `.gitignore` 里明写「`assets/*.png` 是内容不是产物」⇒ 属刻意入库);
· 运行产物仍按既定规则排除(`__pycache__` / `logs/` / `tmp/` / `.venv/` / `*.egg-info` / `uv.lock` / `.workbuddy/`)。
2026-10-08 22:29:08 +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())