用户令逐字:「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/`)。
234 lines
7.9 KiB
Python
234 lines
7.9 KiB
Python
#!/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())
|