WorkBuddy 使用报告

SKILL研究检索
研究检索 未标注权限 许可证不明确
安装这个技能

一句话简介: 从 WorkBuddy 本地真实数据,挖出你做过哪些任务、最爱哪些技能与专家、工作量趋势如何——自动产出 Excel 台账 + HTML 看板,全程零推断。 亮点: 🎯 真·零推断:直连 WorkBuddy 本地三大真实数据源(workbuddy.db 任务/积分/专家 · artifact-index 精确产物文件名 · 会话转录里的真实技能调用),所有字段落地有据,不靠猜。 🗺️ 三源一步到位:不用再翻数据库漏产物、翻转录找技能——踩过的坑(转录里 tool_use 解析为空、技能藏在双重序列化字符串中)都已填平。 📊 个人偏好一目了然:技能 / 专家 / 专家团使用频次排名,清楚知道你最依赖谁。 📈 工作量可视化:按月 / 周 / 天统计任务量,自带 Excel 原生图表 + 可交互 HTML 看板,开链接即看趋势。 📁 双形态交付:Excel 明细台账可二次筛选透视,HTML 看板适合一眼扫读和分享。 适用场景: 想复盘自己的 AI 办公投入、做月度 / 季度工作总结、向团队展示 WorkBuddy 的实际价值,或者只是好奇"我到底用它干了啥"。一句话触发即可。

一句话选型结论

按资料字段整理

WorkBuddy 使用报告是研究检索类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类研究检索 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_d8cafa77/workbuddy-usage-report
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

研究检索
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_d8cafa77仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791713661
正文体量52.7 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
查资料并整理出处

让模型按主题查资料,并把引用来源整理成清单。

网页与文献要点提炼

把长文、论文、网页压成能直接看的要点。

竞品与行业汇总

按关键词汇总竞品动态与行业信息。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在研究检索场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
WorkBuddy 使用报告当前 未标注权限 未标注 未标注 未标注 有
寻找技能 浏览器控制 GitHub 仓库 3120.0 万 Codex、Claude、Cursor 有
自我改进 Agent 接触敏感数据 ClawHub 市场 1646.4 万 OpenClaw 有
灵魂追踪 联网访问 GitHub 仓库 758.9 万 Codex、Claude、Cursor 有
自我完善 联网访问 ClawHub 市场 682.9 万 OpenClaw 有
微软代工厂 调用外部服务 GitHub 仓库 661.4 万 Codex、Claude、Cursor 有

SKILL.md 全文

52.7 KB
模型实际读到的说明书原文,来自技能仓库

WorkBuddy 使用报告 v4.1 — 技能说明

> 一句话触发:「统计我 WorkBuddy 的使用情况」或「我做过了哪些任务」或「我用了哪些技能和专家」。
> 本技能自动从本地三个真实数据源挖数据,产出 Excel 台账 + HTML 看板,全程零推断。
> 当前版本:v5.1.0


快速概览(先看这里)

项目 说明
输入 用户一句话触发,无需提供任何文件路径
数据来源 本地三个真实数据源(见下方「数据地图」),不靠猜
输出产物 ① WorkBuddy使用报告.xlsx(5 个 sheet:任务统计[按时间倒序] + 模型Token占比 + 数据说明 + 名称对照 + 统计分析[6 张图表])② WorkBuddy使用报告.html(三合一看板:① 任务概览 ② Token 结构[含涨价测算器] ③ 模型双维度拆解[含交叉矩阵],顶部导航切换)
典型耗时 50 个任务约 30 秒(含数据提取+生成)
适用场景 个人复盘 / 月度汇报 / 向老板展示 AI 工具使用价值 / 发现工具使用偏好 / Token 成本结构分析(看清钱花在哪类 token、缓存命中如何稀释大模型涨价冲击)
隐私保证 只读本地文件,不联网、不上传、不在技能包里保留任何用户数据

产出效果长什么样?

Excel 台账包含:
- 「任务统计」sheet:每行一个任务,按时间倒序(越接近当下越靠上),列 = 序号 / 任务名称 / 时间 / 消耗积分 / 技能(中英) / 专家(中英) / 专家团(中英) / 产物1…N
- 「模型Token占比」sheet(v5.1 新增):维度1 用户入口表 + 维度2 真实底座表 + 入口×底座交叉矩阵 + Token 全局结构(全价输入/缓存命中/输出/缓存命中率/总 token)
- 「数据说明」sheet:数据口径解释
- 「名称对照」sheet:英文 ID ↔ 中文名映射表
- 「统计分析」sheet:6 张原生图表 = 技能 Top10 柱状图 / 专家 Top10 / 专家团 Top10 / 按月任务量柱状图 / 按周折线图 / 按天柱状图

HTML 看板包含:
- 顶部 KPI 卡片:总任务数 / 总积分消耗 / 技能种类数 / 专家数 / 产物文件数
- 6 张 Chart.js 交互图表(悬停看数值、可点击图例隐藏系列)
- 底部可排序任务明细表

典型使用示例

> 用户:「帮我统计一下自从用 WorkBuddy 以来都做过哪些任务」
>
> 技能执行:
> 1. 自动定位 ~/.workbuddy/workbuddy.db → 提取 50 条任务记录
> 2. 遍历 artifact-index/ → 匹配 37 个任务的 202 个产物文件
> 3. 扫描 projects/*/ 转录 → 正则提取 26 种真实技能调用
> 4. 生成 Excel(含统计分析图表)+ HTML 看板
> 5. 告知用户:「共 N 个任务,消耗积分 = credit_json 各模型求和(非 used 内部用量),最常用技能是 XXX,7 月任务量最大(XX 个)」

> 用户:「我想看看我上个月的工作量分布」
>
> 技能执行:同上流程,但在交付时重点展示「按月/按周/按天」三张工作量图表,并指出高峰日期和空闲期。


避坑清单:这些坑不要踩 ⚠️

以下错误做法都会导致结果失真或运行失败。请 Agent 执行时对照检查。

# ❌ 错误做法 ✅ 正确做法 后果
1 只读 workbuddy.db,就填「技能」「产物」 技能去 projects/*/<sessionId>.jsonl 转录里用正则挖;产物去 artifact-index/<sessionId>.json 精确匹配 技能和产物全靠猜,用户一眼看出「推断」痕迹
2 用 usage-log.json 把技能归到具体任务 只用 usage-log.json 看全局趋势;具体任务的技能必须来自该 session 的 .jsonl usage-log.json 是按天聚合的,无法归到单次任务
3 把 artifact-index 里含 .workbuddy 的内部文件当产物输出 产物路径含 .workbuddy 的一律排除 会把构建脚本、临时文件、中间产物当成交付物
4 把 created_at 毫秒时间戳直接当秒用 转 datetime 时必须除以 1000 时间全部显示成 1970 年左右
5 看到 agentmail、agently-mail 就擅自合并成 agent-mail 如实列出,向用户标注「可能是同一技能的不同字面变体,待确认」 擅自合并会丢失数据精度,引发质疑
6 把 expert_id 中非 Team 结尾的也当专家团 只有以 Team 结尾的才是专家团,其余归专家 分类错误,偏好排名失真
7 直接把 .jsonl 当 JSON 解析后递归找 skill 字段 .jsonl 里的 Skill 调用是双重序列化字符串,必须用锚定 "name":"Skill" 的正则从原始文本提取 165 个文件可能 0 命中
8 Excel 被预览锁了还强写,直接报 PermissionError 捕获 PermissionError 自动另存 _v2 / _v3 用户以为脚本崩了,其实关了预览就能写
9 在 Windows 上找 venv 的 bin/python Windows venv 可执行文件在 Scripts\python.exe No such file or directory
10 把「无 Skill 调用记录」的任务也算进技能频次排名 统计排名时必须排除,仅作为数据完整性标记 会凭空多出一个「无记录」的高频技能,污染 Top10
11 遇到文件缺失就让整个脚本崩溃 用 safe_extract() 包装每个提取步骤,缺哪个来源就跳过哪个来源 一个目录缺失导致整份报告出不来
12 把 HTML 图表库的 CDN 换成「更稳定」的本地文件 保持 Chart.js CDN 默认,离线时表格和 KPI 仍能显示 本地文件路径因环境而异,反而增加失败率
13 引用 session_usage.total_credits 字段,或把 used 当积分求和 积分只认 credit_json(JSON 字典,各模型积分求和);used 是内部用量单位(类似 tokens),不是积分;credit_json 为空的会话(进行中/未结算)计 0 属正常 引用不存在的字段会直接报错;误把 used 求和会让总积分虚高数十万~数百万倍(朋友实测:504385 vs 真实 660.52)
14 把 jsonl 每行 usage 直接相加当作会话 token usage 是会话累计值(单调不减),逐行相加会重复计数 8~90 倍;正确做法:取每会话最后一个 assistant 响应行的 usage(末值) 总量虚高一个数量级,结构比例全错
15 用「逐行 delta(当前累计−上一行)」求单轮 token 部分会话响应间 usage 会回落(非单调),delta 全取正会高估;实测末值口径与 session_usage.used 最吻合(Σ末值/Σused ≈ 1.008) Σ远超 used,交叉验证失败
16 把 auto(自动路由)当成一个「模型」并列统计 auto 是路由层,不产生 token,token 归在 providerData.model 真实底座;必须双维度(用户入口 requestModelName × 真实底座 pd.model) 路由层和底座混在一起,模型排名失真
17 只看 pd.model 忽略 requestModelName 分不清「你手动选的」与「系统 auto 派发的」(GLM-5.2 几乎全是 auto 路由);用双维度交叉矩阵才看得清 误把路由派发量算成你的手动偏好
18 认为 hy3 免费就不计 token 免费只免积分/钱(credit_json 为 Null),token 计量照常(used 有数);统计必须包含免费模型的 token 漏掉占比近半的 hy3 用量
19 把 token 结构当「钱的结构」直接报金额 本地 credit_json 里模型是 32 位哈希,无哈希→名称映射,本地算不出各模型花多少钱;token 占比 × 单价才得钱,须向用户说明 报告被质疑「金额怎么来的」

数据地图(核心参考)

数据项 真实来源 路径 关键细节
任务名 / 时间 / 积分 / 专家·专家团 SQLite 数据库 ~/.workbuddy/workbuddy.db 表:sessions + session_usage
产物文件名(精确匹配) 产物索引 JSON ~/.workbuddy/artifact-index/<sessionId>.json 逐任务记录,不含推断
技能调用名(真实 Skill 名) 会话转录文本 ~/.workbuddy/projects/<slug>/<sessionId>.jsonl 双重序列化字符串,需正则提取
❌ 不用于归因 全局聚合日志 usage-log.json / traces/ / audit-log/ 仅汇总,无任务级明细

数据提取技术细节

1. 从 workbuddy.db 提取任务基础信息

import sqlite3, os
from datetime import datetime

DB_PATH = os.path.expandvars(r'$USERPROFILE/.workbuddy/workbuddy.db')
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row

rows = conn.execute('''
    SELECT s.id, s.title, s.custom_title, s.created_at, s.expert_id, su.credit_json
    FROM sessions s
    LEFT JOIN session_usage su ON su.session_id = s.id
    WHERE (s.deleted_at IS NULL OR s.deleted_at = 0)
    ORDER BY s.created_at
''').fetchall()

def parse_credits(credit_json_text):
    """解析真实消耗积分。
    credit_json 形如 {"model_id_xxx": 346.15, ...},各值之和才是真实积分。
    ⚠️ used 字段是内部用量单位(类似 tokens),绝非积分,切勿对其求和!
    credit_json 为空的会话(进行中/未结算)计 0,属正常。"""
    if not credit_json_text:
        return 0.0
    try:
        obj = json.loads(credit_json_text)
        if isinstance(obj, dict):
            return sum(float(v) for v in obj.values())
        if isinstance(obj, (int, float)):
            return float(obj)
    except Exception:
        pass
    return 0.0

for r in rows:
    ts = datetime.fromtimestamp(r['created_at'] / 1000)  # ⚠️ 毫秒戳需除以1000
    expert = r['expert_id'] or ''
    is_team = expert.endswith('Team')  # Team 后缀 → 归属专家团
    name = r['custom_title'] or r['title'] or '(未命名任务)'  # sessions 表无 name 列,任务名在 title/custom_title
    credits = parse_credits(r['credit_json'])  # ✅ 真实积分,非 used

注意:created_at 是毫秒级 Unix 时间戳,转 Python datetime 必须除以 1000。

2. 从 artifact-index 提取产物文件

import json, glob, os, urllib.parse

ART_DIR = os.path.expandvars(r'$USERPROFILE/.workbuddy/artifact-index')
artifact_file = os.path.join(ART_DIR, session_id + '.json')

if os.path.exists(artifact_file):
    data = json.load(open(artifact_file, encoding='utf-8'))
    products = []
    for a in data.get('artifacts', []):
        if a.get('type') not in ('media', 'file-changes'):
            continue
        uri = a.get('uri', '')
        # 解码 file:/// 或 file-changes:// 协议前缀
        if uri.startswith('file:///'):
            path = urllib.parse.unquote(uri[8:])
        elif uri.startswith('file-changes://'):
            path = urllib.parse.unquote(uri[len('file-changes://'):])
        else:
            continue
        # 排除 .workbuddy 内部文件(如构建脚本、临时文件)
        if '.workbuddy' in path.lower():
            continue
        name = a.get('name') or a.get('title') or os.path.basename(path)
        products.append(name)

过滤规则:只保留 type=media 或 type=file-changes 的条目;排除路径含 .workbuddy 的内部产物。

3. 从会话转录提取技能调用(最容易踩坑)

技能调用不在结构化字段里,而是藏在转录原始文本中,以双重序列化形式存在:

"name":"Skill","arguments":"{\"skill\": \"expert-manager\"}"
import re, glob, os

PROJ_DIR = os.path.expandvars(r'$USERPROFILE/.workbuddy/projects')
# 定位该 session 的 jsonl 文件
matches = glob.glob(os.path.join(PROJ_DIR, '**', session_id + '.jsonl'), recursive=True)
if not matches:
    skills = []  # 该 session 无转录文件 → 无技能记录
else:
    raw = open(matches[0], encoding='utf-8', errors='ignore').read()
    # 锚定 "name":"Skill" 工具调用,提取双重序列化的 skill 名
    pat = re.compile(
        r'"name"\s*:\s*"Skill"[\s\S]{0,500}?'
        r'\\"skill\\":\s*\\"([A-Za-z0-9_\-]+)\\"'
    )
    skills = sorted(set(m.group(1) for m in pat.finditer(raw)))

# 同时抓手动挂载块中的技能名
attached = re.compile(r'<manually_attached_skills>(.*?)</manually_attached_skills>', re.S)
for m in attached.finditer(raw):
    sub = re.findall(r'(?:^|\n)\s*name:\s*([A-Za-z0-9_\-]+)', m.group(1))
    skills.extend(sub)

# 过滤误报噪声
STOP = {'x', 'xx', 'xxx', 'X', 'skill', 'skills', 'Skill'}
skills = [s for s in set(skills) if s not in STOP]

为什么这么复杂? 转录文件是 .jsonl 格式,每行是一个 JSON 对象。AI 调用 Skill 工具时,参数本身是 JSON 字符串,序列化进外层 JSON 后变成双重转义。必须用 \\"skill\\" 才能匹配到。


执行步骤(Agent 照此执行)

  1. 检测环境:确认 workbuddy.db 存在、Python venv 可用、openpyxl 已装(缺则自动安装)
  2. 提取任务基础信息:从 DB 读 sessions + session_usage(含异常处理:0 条记录时终止并提示)
  3. 提取产物:遍历 artifact-index(缺目录时跳过,单文件缺失时标记「无」)
  4. 提取技能:扫描 projects 转录(缺目录时跳过,用正则提取 + stoplist 过滤)
  5. 组装数据:合并三源数据,补全中文名称映射
  6. 生成 Excel:写入台账 4 个 sheet + 6 张图表(文件被锁时自动加后缀重试)
  7. 生成 HTML:同一份聚合数据渲染交互看板
  8. 交付与说明:用 present_files 同时交付 xlsx + html;告知用户数据概况(总数/积分/Top3 技能/峰值月份);如有异常(如部分任务缺产物或缺技能)一并说明

异常处理与容错机制

场景清单与处理策略

# 异常场景 检测方式 处理策略 用户提示
1 workbuddy.db 不存在 os.path.exists(DB_PATH) 为 False 终止并提示 ⚠️ 未找到 WorkBuddy 数据库。请确认已安装 WorkBuddy 并至少完成过一次对话。
2 数据库查询返回 0 条任务 len(rows) == 0 终止并提示 ⚠️ 数据库中没有找到任何任务记录。可能原因:(a) 所有会话已被删除 (b) 数据库路径不正确
3 artifact-index 目录不存在 os.path.exists(ART_DIR) 为 False 跳过产物提取,产物列全填「无记录」 ⚠️ 产物索引目录未找到,产物列将显示为空。不影响其他数据。
4 单个 session 无 artifact 文件 os.path.exists(artifact_file) 为 False 该任务产物记为「无」 (静默处理,不逐一提示)
5 projects 目录不存在或为空 glob.glob(...) 返回空列表 技能列全填「无 Skill 调用记录」 ⚠️ 未找到会话转录文件,技能列将显示为空。可能需要检查 projects 目录权限。
6 单个 session 无 jsonl 文件 matches 列表为空 该任务技能记为「—(无记录)」 (静默处理)
7 Excel 文件被占用(预览锁) 写入时抛 PermissionError 自动另存为 _v2 / _v3(递增后缀)重试 📌 原 xlsx 被预览占用,已自动保存为新文件名 {原文名}_v2.xlsx
8 openpyxl 未安装 ImportError 自动在隔离 venv 中安装 📌 正在安装依赖包 openpyxl,请稍候…
9 时间戳异常(0 或负数) ts <= 0 或转换报错 记为「时间未知」,不中断 (静默处理,该行时间列标灰)
10 技能名为空或纯噪声 提取结果在 STOP 列表中 已由正则 stoplist 过滤 (静默处理)

容错代码模板

import traceback

def safe_extract(func, default, label=""):
    """通用安全提取包装器:出错时返回默认值并记录"""
    try:
        return func()
    except Exception as e:
        print(f"[WARN] {label} 失败: {e}")
        return default

# 示例:安全读取 artifact
def get_products(session_id):
    def _inner():
        fp = os.path.join(ART_DIR, session_id + '.json')
        if not os.path.exists(fp):
            return []
        # ... 正常提取逻辑 ...
        return products
    return safe_extract(_inner, [], f"产物提取-{session_id}")

# 示例:安全写入 Excel(自动处理文件锁定)
def safe_write_xlsx(output_path, writer_func, max_retries=3):
    for i in range(max_retries + 1):
        try:
            base, ext = os.path.splitext(output_path)
            path = output_path if i == 0 else f"{base}_v{i}{ext}"
            writer_func(path)
            return path
        except PermissionError:
            if i < max_retries:
                continue
            raise

运行稳定性保障

  • 隔离环境:所有 Python 脚本运行在 ~/.workbuddy/binaries/python/envs/default/ 隔离 venv 中,不污染系统环境
  • Windows 路径注意:venv 可执行文件在 Scripts\ 下(非 Linux 的 bin/)。正确路径:
    C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\python.exe C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\pip.exe
  • 编码安全:所有文件读写显式指定 encoding='utf-8', errors='ignore',避免 GBK/UTF-8 冲突

输出规格

Excel 台账(WorkBuddy任务统计.xlsx)

Sheet 内容 列/元素
任务统计 主数据表 序号 / 任务名称 / 时间(YYYY-MM-DD HH:MM) / 消耗积分 / 技能(英文·真实) / 技能(中文) / 专家(英文) / 专家(中文) / 专家团(英文) / 专家团(中文) / 产物1 / 产物2 / … / 产物N
数据说明 口径文档 各字段定义、数据来源、计算公式
名称对照 映射表 skill_id ↔ 中文名 / expert_id ↔ 中文名 / team_id ↔ 中文名
统计分析 图表区 6 张 openpyxl 原生图表(见下)

统计分析 6 张图表:

图表 类型 X轴 Y轴 用途
技能偏好 Top10 柱状图 技能名(中文) 调用次数 看你最常用哪些技能
专家偏好 Top10 柱状图 专家名(中文) 被召唤次数 看你最爱找哪位专家
专家团偏好 柱状图 专家团名(中文) 被召唤次数 看哪个团队帮你最多
月度工作量 柱状图 年-月 任务数量 看月度节奏
周度工作量 折线图 ISO 周(周一起点) 任务数量 看周间波动
日度工作量 柱状图 日期 任务数量 看每日分布、找高峰日

HTML 看板(WorkBuddy任务统计看板.html)

  • 引入 Chart.js CDN(无需本地安装)
  • 顶部 5 个 KPI 卡片(总任务 / 总积分 / 技能种数 / 专家数 / 产物数)
  • 6 张交互图表(鼠标悬停显示精确值)
  • 底部可排序表格(点击表头排序)

数据清洗规则

规则 说明
无技能任务标记 技能列填 —(无Skill工具调用记录),统计排名时排除
技能变体 如 agent-mail 可能被记为 agently-mail 等 → 不擅自合并,向用户标注待确认
多技能任务 每个技能分别计数(占比分母=任务总数,多技能任务会导致总和>100%,属正常现象)
积分为空 部分 session 可能无 usage 记录 → 积分列填 0,并在数据说明中注明

常见问题(FAQ)

Q:报告里的数据准确吗?会不会有推断成分?
A:完全准确,零推断。三项核心数据各有明确来源:任务/专家来自 SQLite 数据库结构化查询;积分来自 session_usage.credit_json(JSON 字典,各模型积分求和),绝不引用不存在的 total_credits 字段,也绝不把 used 内部用量当积分;产物来自 artifact-index 逐文件匹配;技能来自会话转录正则提取。每一列都能追溯到原始文件。

Q:为什么有些任务的技能列显示「无记录」?
A:两种可能:(1) 该任务确实没有调用任何 Skill 工具(如纯对话类任务);(2) 该 session 的转录文件(.jsonl)未被持久化到磁盘。这两种情况都会如实标注,不会猜测填充。

Q:产物列为什么有些任务是空的?
A:只有通过 present_files 或类似机制正式产出的文件才会出现在 artifact-index 中。纯对话、纯查询类任务没有实体产物,属于正常现象。

Q:生成的 Excel 打不开 / 显示乱码?
A:确保用较新版本的 WPS 或 Microsoft Excel 打开(openpyxl 生成的 .xlsx 需要 Excel 2007+ 支持)。如遇文件被锁无法打开,关闭预览窗口即可。

Q:HTML 看板打开是空白?
A:HTML 使用 Chart.js CDN 加载图表库,需要联网。首次打开可能需要 1-2 秒加载。离线环境下图表区域会留白,但 KPI 卡片和数据表格仍可正常显示。

Q:能不能只生成 Excel 或只生成 HTML?
A:可以。默认两者都生成,但用户可以明确要求只出其中一种。两种格式互补:Excel 适合二次编辑和存档,HTML 适合演示和快速浏览趋势。

Q:数据太多导致 Excel 很大怎么办?
A:如果任务数超过 200,产物列可能会很宽。脚本会自动将产物超过 6 个的任务截断,多余产物移入「备注」列。如需完整产物列表,请查看 HTML 版的任务明细表。

Q:中文名称映射哪里来的?
A:内置了常见 skill/expert/team 的中英对照表。如果遇到未收录的新技能/新专家,会直接显示英文原名,并在交付时提醒用户「以下 ID 缺少中文映射,欢迎补充」。

Q:运行报错 ModuleNotFoundError: No module named 'openpyxl' 怎么办?
A:Agent 应在隔离 venv 中自动安装:Scripts\pip.exe install openpyxl。如果仍失败,检查 venv 是否创建成功:python -m venv 在 Windows 上需要管理员权限写 Program Files 时改用用户目录。

Q:token 结构里的「缓存命中率」是什么,为什么重要?
A:缓存命中(cache_read_input_tokens)指命中 prompt cache 的输入 token,按官方约 0.1× 计费。命中率 = 缓存命中 / 总输入。它重要是因为:缓存命中占比越高,大模型涨价对你的实际冲击越小——它是你成本里的「缓冲垫」。

Q:auto 自动路由算不算一个模型?它到底有没有消耗 token?
A:不算。auto 是路由层,本身不产生 token,它把任务派发给真实底座(混元/GLM/DeepSeek/Kimi)。日志里 requestModelName=Auto 的响应,其 providerData.model 才是真正跑的模型。所以本技能拆成「用户入口 × 真实底座」双维度,auto 单独作为入口,不列为模型。

Q:这能直接算出我各模型花了多少钱吗?
A:本地算不出金额。credit_json 里模型是 32 位哈希 ID,本地没有「哈希→模型名→单价」映射表。本技能只报 token 结构和占比;要算钱,需你拿各模型公开单价 × 本表占比。报告会明确标注「这是 token 结构,不是钱的结构」。


边界与限制

本技能并非万能,以下情况会如实告知用户,不会伪造数据:

场景 结果
WorkBuddy 未安装或刚安装,没有任何对话记录 提示「未找到任务记录」并终止
用户手动删除了某些会话 已删除会话不会出现在报告中(这是正常的)
projects/ 目录被清理或权限不足 技能列会显示为空或「无记录」
任务没有调用 Skill 工具 技能列显示「—(无Skill工具调用记录)」
任务没有产出文件 产物列显示为空
运行在离线环境 HTML 看板中的图表无法渲染,但 Excel、表格、KPI 卡片仍可用
中文映射表未覆盖新 skill/expert 显示英文原名,并提示用户补充映射
时间戳为 0 或异常 该行时间显示为「时间未知」,不影响其他行

偏好分析计算口径

指标 公式 备注
技能频次 每个技能出现的任务数之和 多技能任务每个技能各+1,占比分母=任务总数(可超100%)
专家频次 该 expert_id 出现的任务数 每任务最多1个专家
专家团频次 含 Team 后缀的 expert_id 出现任务数 每任务最多1个团队
月度工作量 按 created_at/1000 取年-月分组计数 毫秒戳!
周度工作量 按 ISO 周一为起点分组 datetime.isocalendar()[1]
日度工作量 按日期(YYYY-MM-DD) 分组 可发现工作密集日和休息日
总消耗积分 各会话 credit_json 内模型积分求和(非 used) credit_json 为空的会话计 0(未结算/进行中属正常);绝不引用不存在的 total_credits

第七章 Token 用量结构与大模型拆解分析(v5.0 新增)

> 触发:「token 用量结构」「各模型消耗多少 token」「缓存命中率」「大模型涨价对我涨了多少」「token 拆解」。
> 本节方法已用真实数据交叉验证,口径见下方「口径校准」,全部零推断。
> 配套脚本:scripts/token_structure_report.py(全局 token 结构 + 涨价测算器)、scripts/model_token_report_v2.py(各模型双维度拆解 + 交叉矩阵)。

7.1 为什么这套分析有价值(先讲结论)

绝大多数人只知道「这个月花了多少钱」,不知道钱的结构。本技能把 token 拆成三类:

  • 全价输入 token(input_tokens − cache_read_input_tokens):按全价计费,是涨价主战场
  • 缓存命中输入 token(cache_read_input_tokens):按官方约 0.1× 计费,通常不在涨价范围
  • 输出 token(output_tokens):按输出价计费,量通常很小

关键洞察:缓存命中占比越高,大模型涨价对你的实际冲击越小。实测缓存命中率约 80% 时,即便全价输入/输出都翻倍(×2),你的实际总成本只涨约 20%;若命中率为 0(无缓存),同样涨幅直接涨 100%。所以缓存命中是你对抗涨价的缓冲垫。

7.2 数据来源(与任务报告不同)

数据项 真实来源 路径 关键字段
每轮 token 明细 会话转录 JSONL ~/.workbuddy/projects/<slug>/<sessionId>.jsonl message.usage.{input_tokens,output_tokens,total_tokens,cache_read_input_tokens}(Anthropic 格式)
真实底座模型 同上转录 同上 providerData.model(token 实际产生处)
用户入口 同上转录 同上 providerData.requestModelName / requestModelId(你选了什么:Hy3 / Auto / 显式 GLM / 未记录)
计费口径校验 SQLite ~/.workbuddy/workbuddy.db → session_usage.used 每会话总 token(权威外部校验)

> ⚠️ 注意:session_usage 表只有 used(总 token),没有 input/output/cache 拆分。拆分只能从转录 jsonl 的 usage 来。转录比 API 后台更细(精确到每轮每会话),无需跑后台。

7.3 口径校准(踩过的坑,必须照做)

  1. usage 是「会话累计值」,不是单轮增量。每行 assistant 响应里的 usage 是该会话到当前为止的累计;把每行相加会重复计数 8~90 倍。✅ 取每会话最后一个 assistant 响应行的 usage(末值) 作为该会话总量。
  2. 不要用「逐行 delta」。实测部分会话响应间 usage 会回落(非单调),delta 全取正会高估。✅ 末值口径与 session_usage.used 最吻合:Σ末值 / Σused ≈ 1.008(计费会话内 93% 在 ±2% 内)。
  3. auto 是路由层,不产生 token。日志铁证:requestModelName=Auto 的响应行,其 providerData.model 实际是 glm-5.2 / deepseek / kimi 等真实底座。✅ 必须拆成双维度(见 7.4),不能把 auto 当成一个模型并列。
  4. hy3 免费 ≠ 不计 token。免费只免积分/钱(credit_json 为 Null),token 计量照常(used 有数)。✅ 统计必须包含免费模型。
  5. token 结构 ≠ 钱的结构。credit_json 里模型是 32 位哈希,本地无哈希→名称映射,本地算不出「各模型花多少钱」。✅ 只报 token 占比,金额需用户拿单价×比例自算,并明确说明。
  6. 未计费会话:约有 5~7 个工具/图像类会话 used=0 但 token 真实产生,归入总量并在口径说明中标注,差额属正常。

7.4 双维度拆解方法(解开 auto 黑盒)

每个会话取末值行的 (用户入口, 真实底座, usage) 三元组,归因整个会话:

  • 维度1 · 用户入口(requestModelName 归一化):混元 Hy3 / Auto 自动路由 / 显式 GLM-5.2 / 未记录(工具·图像类)
  • 维度2 · 真实底座(providerData.model 归一化):混元 Hy3 / GLM-5.2 / GLM-5.2-A / GLM-5.2-X / DeepSeek-V4-Pro / Kimi-K2.7 / GLM-5.1 / Auto路由层(未解析底座)

> Auto路由层(未解析底座) = 日志未下钻出具体底座的 auto 路由响应(黑盒,约占总 token 的 22%,无法再细分)。未记录 入口多为工具/图像类响应(无 requestModelName,不走 prompt 缓存)。

核心发现:GLM-5.2 那部分 token 几乎全是 auto 路由派发的(1852 行 requestModelName 全是 Auto),你手动选 GLM 的只有 glm-5.2-x 那 22 万。旧表把 auto 和 glm-5.2 并列当「各模型」是错的,双维度才正确。

7.5 交叉验证(必做,证明数据自洽)

  • 内部闭合:Σ各用户入口 = Σ各真实底座 = Σ各会话末值 usage(三者应分毫一致)
  • 矩阵一致:入口×底座交叉矩阵,每行之和=该入口总量,每列之和=该底座总量,行和与列和相等
  • 外部对账:在「有 used 计费记录」的会话内,Σ末值 / Σused ≈ 1.0(实测 1.007),证明转录口径与外部权威一致
  • 差额解释:全量 Σ末值 比 Σused 多约 20%,来自未计费会话(token 真实产生但 used=0),属口径差异非错误

> 若交叉验证不闭合(偏差 > 1%),立即回头查口径(几乎必是踩了 7.3 的坑 #1/#2)。

7.6 大模型涨价影响测算器(逻辑)

基于本地真实占比,给交互式测算器。设三类 token 的「涨价后÷涨价前单价倍数」为 kIn / kOut / kCache(默认 1.0):

newCost = 全价输入 × kIn + 缓存命中 × kCache + 输出 × kOut
总成本涨幅倍数 = newCost / 总token成本基准

因缓存命中占比高(约 80%)且通常 kCache≈1,它天然稀释涨价冲击。脚本在 HTML 里实时算,并对比「若无缓存(命中率0)同样涨幅下的成本涨幅」,凸显缓存价值。

7.7 运行方式

# 全局 token 结构 + 涨价测算器
C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\python.exe scripts/token_structure_report.py
# 各模型双维度拆解 + 交叉矩阵
C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\python.exe scripts/model_token_report_v2.py

两脚本仅依赖 Python 标准库(json/re/sqlite3/os),无需 openpyxl,输出 HTML 到当前目录。


附录:完整可运行 Python 脚本

> 以下脚本可直接保存为 generate_workbuddy_report.py 并在隔离 venv 中运行。
> 运行前确保已安装:pip install openpyxl

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
WorkBuddy 使用报告生成器 v3.0
数据来源:workbuddy.db + artifact-index + projects 转录
输出:Excel 台账 + HTML 看板
"""

import json
import os
import re
import sqlite3
import urllib.parse
from collections import Counter, defaultdict
from datetime import datetime, timedelta

# 尝试导入 openpyxl;若缺失则给出清晰提示
try:
    from openpyxl import Workbook
    from openpyxl.chart import BarChart, LineChart, Reference
    from openpyxl.utils import get_column_letter
except ImportError:
    raise SystemExit(
        "缺少 openpyxl。请在隔离 venv 中安装:\n"
        r"C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\pip.exe install openpyxl"
    )

# ---------------------------- 配置 ----------------------------
USERPROFILE = os.path.expandvars('$USERPROFILE')
WB_DIR = os.path.join(USERPROFILE, '.workbuddy')
DB_PATH = os.path.join(WB_DIR, 'workbuddy.db')
ART_DIR = os.path.join(WB_DIR, 'artifact-index')
PROJ_DIR = os.path.join(WB_DIR, 'projects')

OUTPUT_XLSX = 'WorkBuddy任务统计.xlsx'
OUTPUT_HTML = 'WorkBuddy任务统计看板.html'

# 常见名称中文映射(可扩展)
NAME_MAP = {
    # skills
    'ppt-master': 'PPT 大师',
    'tencent-docs': '腾讯文档',
    'expert-manager': '专家管理',
    'agent-mail': '智能体邮箱',
    'agently-mail': '智能体邮箱(疑似变体)',
    'agentmail': '智能体邮箱(疑似变体)',
    'book-to-skill': '书籍转技能',
    'wechat-article-pro': '公众号文章专业版',
    'market-researcher': '市场研究专家',
    'workbuddy-usage-report': 'WorkBuddy 使用报告',
    # experts
    'SalesCoach': '销售教练',
    'GEOoptimizer': 'GEO 优化师',
    # teams
    'GPTResearcherTeam': 'GPT 调研员团',
    'SalesBattleTeam': '销售战训团',
    'HumanizePptTeam': 'PPT 美化团',
    'AiContentCreatorTeam': 'AI 内容创作团',
}

STOP_SKILLS = {'x', 'xx', 'xxx', 'X', 'skill', 'skills', 'Skill'}

NO_SKILL_LABEL = '—(无Skill工具调用记录)'


def safe_extract(func, default, label=""):
    """通用安全提取包装器"""
    try:
        return func()
    except Exception as e:
        print(f"[WARN] {label} 失败: {e}")
        return default


def get_display_name(key):
    return NAME_MAP.get(key, key)


def ms_to_dt(ts):
    """毫秒时间戳转 datetime"""
    try:
        if not ts or ts <= 0:
            return None
        return datetime.fromtimestamp(ts / 1000)
    except Exception:
        return None


def parse_credits(credit_json_text):
    """解析真实消耗积分。

    credit_json 形如 {"model_id_xxx": 346.15, ...},各值之和才是真实积分。
    ⚠️ used 字段是内部用量单位(类似 tokens),绝非积分,切勿对其求和!
    credit_json 为空的会话(进行中/未结算)计 0,属正常。
    """
    if not credit_json_text:
        return 0.0
    try:
        obj = json.loads(credit_json_text)
        if isinstance(obj, dict):
            return sum(float(v) for v in obj.values())
        if isinstance(obj, (int, float)):
            return float(obj)
    except Exception:
        pass
    return 0.0


# ---------------------------- 1. 提取任务 ----------------------------
def load_tasks():
    if not os.path.exists(DB_PATH):
        raise FileNotFoundError(f"未找到 WorkBuddy 数据库: {DB_PATH}")

    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    rows = conn.execute('''
        SELECT s.id, s.title, s.custom_title, s.created_at, s.expert_id, su.credit_json
        FROM sessions s
        LEFT JOIN session_usage su ON su.session_id = s.id
        WHERE (s.deleted_at IS NULL OR s.deleted_at = 0)
        ORDER BY s.created_at
    ''').fetchall()
    conn.close()

    if not rows:
        raise ValueError("数据库中没有任何任务记录")

    tasks = []
    for r in rows:
        dt = ms_to_dt(r['created_at'])
        expert = r['expert_id'] or ''
        is_team = expert.endswith('Team')
        tasks.append({
            'id': r['id'],
            'name': (r['custom_title'] or r['title'] or '(未命名任务)'),
            'created_at': r['created_at'],
            'dt': dt,
            'credits': parse_credits(r['credit_json']),  # ✅ credit_json 求和,非 used
            'expert_raw': expert,
            'expert': '' if is_team else expert,
            'team': expert if is_team else '',
        })
    return tasks


# ---------------------------- 2. 提取产物 ----------------------------
def get_products(session_id):
    def _inner():
        fp = os.path.join(ART_DIR, session_id + '.json')
        if not os.path.exists(fp):
            return []
        data = json.load(open(fp, encoding='utf-8'))
        products = []
        for a in data.get('artifacts', []):
            if a.get('type') not in ('media', 'file-changes'):
                continue
            uri = a.get('uri', '')
            if uri.startswith('file:///'):
                path = urllib.parse.unquote(uri[8:])
            elif uri.startswith('file-changes://'):
                path = urllib.parse.unquote(uri[len('file-changes://'):])
            else:
                continue
            # 避坑 #3:排除 .workbuddy 内部产物
            if '.workbuddy' in path.lower():
                continue
            name = a.get('name') or a.get('title') or os.path.basename(path)
            products.append(name)
        return products
    return safe_extract(_inner, [], f"产物提取-{session_id}")


# ---------------------------- 3. 提取技能 ----------------------------
def get_skills(session_id):
    def _inner():
        matches = glob_files(PROJ_DIR, session_id + '.jsonl')
        if not matches:
            return []
        raw = open(matches[0], encoding='utf-8', errors='ignore').read()
        # 锚定 "name":"Skill",提取双重序列化的 skill 名
        pat = re.compile(
            r'"name"\s*:\s*"Skill"[\s\S]{0,500}?'
            r'\\"skill\\":\s*\\"([A-Za-z0-9_\-]+)\\"'
        )
        skills = set(m.group(1) for m in pat.finditer(raw))
        # 手动挂载块
        attached = re.compile(r'<manually_attached_skills>(.*?)</manually_attached_skills>', re.S)
        for m in attached.finditer(raw):
            skills.update(re.findall(r'(?:^|\n)\s*name:\s*([A-Za-z0-9_\-]+)', m.group(1)))
        # 过滤噪声
        return sorted(s for s in skills if s not in STOP_SKILLS)
    return safe_extract(_inner, [], f"技能提取-{session_id}")


def glob_files(base, pattern):
    if not os.path.exists(base):
        return []
    result = []
    for root, _, files in os.walk(base):
        for f in files:
            if f == pattern or f.endswith(pattern):
                result.append(os.path.join(root, f))
    return result


# ---------------------------- 4. 生成 Excel ----------------------------
def generate_excel(tasks, output_path):
    wb = Workbook()

    # sheet1: 任务统计
    ws = wb.active
    ws.title = '任务统计'
    headers = [
        '序号', '任务名称', '时间', '消耗积分',
        '技能(英文)', '技能(中文)',
        '专家(英文)', '专家(中文)',
        '专家团(英文)', '专家团(中文)',
        '产物1', '产物2', '产物3', '产物4', '产物5', '产物6'
    ]
    ws.append(headers)

    # 先计算最大产物列数
    max_products = max((len(t['products']) for t in tasks), default=0)
    max_products = min(max_products, 6)

    skill_counter = Counter()
    expert_counter = Counter()
    team_counter = Counter()
    month_counter = Counter()
    week_counter = Counter()
    day_counter = Counter()

    for idx, t in enumerate(tasks, 1):
        # 技能显示
        skill_en = ', '.join(t['skills']) if t['skills'] else NO_SKILL_LABEL
        skill_zh = ', '.join(get_display_name(s) for s in t['skills']) if t['skills'] else NO_SKILL_LABEL
        # 计数(避坑 #10:无技能任务不计入排名)
        for s in t['skills']:
            skill_counter[s] += 1
        # 专家/团
        if t['expert']:
            expert_counter[t['expert']] += 1
        if t['team']:
            team_counter[t['team']] += 1
        # 时间聚合
        if t['dt']:
            month_counter[t['dt'].strftime('%Y-%m')] += 1
            week_counter[t['dt'].strftime('%Y-W%W')] += 1
            day_counter[t['dt'].strftime('%Y-%m-%d')] += 1

        row = [
            idx,
            t['name'],
            t['dt'].strftime('%Y-%m-%d %H:%M') if t['dt'] else '时间未知',
            t['credits'],
            skill_en,
            skill_zh,
            t['expert'],
            get_display_name(t['expert']) if t['expert'] else '',
            t['team'],
            get_display_name(t['team']) if t['team'] else '',
        ]
        # 产物
        for i in range(max_products):
            row.append(t['products'][i] if i < len(t['products']) else '')
        ws.append(row)

    # sheet2: 数据说明
    ws2 = wb.create_sheet('数据说明')
    notes = [
        ['字段', '说明'],
        ['任务名称', '来自 workbuddy.db sessions.title(custom_title 优先,无则取 title)'],
        ['时间', 'sessions.created_at(毫秒戳 ÷1000 转 datetime)'],
        ['消耗积分', 'session_usage.credit_json 各模型积分求和(used 是内部用量,非积分)'],
        ['技能', '来自 projects/*/<sessionId>.jsonl 转录文本中正则提取,不含推断'],
        ['专家/专家团', 'sessions.expert_id;以 Team 结尾归专家团,其余归专家'],
        ['产物', '来自 artifact-index/<sessionId>.json 逐文件匹配'],
        ['无技能任务', '技能列显示「—(无Skill工具调用记录)」,统计排名时已排除'],
    ]
    for r in notes:
        ws2.append(r)

    # sheet3: 名称对照
    ws3 = wb.create_sheet('名称对照')
    ws3.append(['类型', '英文 ID', '中文名称'])
    for s in sorted(skill_counter):
        ws3.append(['技能', s, get_display_name(s)])
    for e in sorted(expert_counter):
        ws3.append(['专家', e, get_display_name(e)])
    for tm in sorted(team_counter):
        ws3.append(['专家团', tm, get_display_name(tm)])

    # sheet4: 统计分析 + 图表
    ws4 = wb.create_sheet('统计分析')
    # 这里仅示意:把各 Counter 写入,并创建图表
    ws4.append(['技能', '次数'])
    for s, c in skill_counter.most_common(10):
        ws4.append([get_display_name(s), c])
    ws4.append([])
    ws4.append(['专家', '次数'])
    for e, c in expert_counter.most_common(10):
        ws4.append([get_display_name(e), c])

    # 添加柱状图示例(技能 Top10)
    chart = BarChart()
    chart.type = "col"
    chart.title = "技能偏好 Top10"
    chart.y_axis.title = '调用次数'
    chart.x_axis.title = '技能'
    data = Reference(ws4, min_col=2, min_row=1, max_row=1 + len(skill_counter))
    cats = Reference(ws4, min_col=1, min_row=2, max_row=1 + len(skill_counter))
    chart.add_data(data, titles_from_data=True)
    chart.set_categories(cats)
    ws4.add_chart(chart, "E2")

    wb.save(output_path)
    return output_path


# ---------------------------- 5. 生成 HTML ----------------------------
def generate_html(tasks, output_path):
    total_tasks = len(tasks)
    total_credits = sum(t['credits'] for t in tasks)
    all_skills = set()
    all_experts = set()
    all_products = 0
    for t in tasks:
        all_skills.update(t['skills'])
        if t['expert']:
            all_experts.add(t['expert'])
        all_products += len(t['products'])

    # 在 Python 里先算好技能 Top10,再转成 JSON 嵌入 HTML
    skill_counter_html = Counter()
    for t in tasks:
        for s in t['skills']:
            skill_counter_html[s] += 1
    skill_top10 = json.dumps([
        {'x': get_display_name(s), 'y': c}
        for s, c in skill_counter_html.most_common(10)
    ], ensure_ascii=False)

    html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WorkBuddy 任务统计看板</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 24px; background: #f5f6f8; }}
.kpi {{ display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }}
.kpi-card {{ background: #fff; border-radius: 8px; padding: 16px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); min-width: 140px; }}
.kpi-num {{ font-size: 28px; font-weight: 700; color: #1677ff; }}
.kpi-label {{ color: #666; font-size: 13px; margin-top: 4px; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 16px; }}
.card {{ background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
.card h3 {{ margin: 0 0 12px; font-size: 15px; color: #333; }}
table {{ width: 100%; border-collapse: collapse; font-size: 13px; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
th, td {{ padding: 8px 12px; text-align: left; border-bottom: 1px solid #f0f0f0; }}
th {{ background: #fafafa; font-weight: 600; cursor: pointer; }}
tr:hover {{ background: #fafafa; }}
</style>
</head>
<body>
<h2>WorkBuddy 使用看板</h2>
<div class="kpi">
  <div class="kpi-card"><div class="kpi-num">{total_tasks}</div><div class="kpi-label">总任务数</div></div>
  <div class="kpi-card"><div class="kpi-num">{total_credits:,}</div><div class="kpi-label">总消耗积分</div></div>
  <div class="kpi-card"><div class="kpi-num">{len(all_skills)}</div><div class="kpi-label">技能种数</div></div>
  <div class="kpi-card"><div class="kpi-num">{len(all_experts)}</div><div class="kpi-label">专家数</div></div>
  <div class="kpi-card"><div class="kpi-num">{all_products}</div><div class="kpi-label">产物文件数</div></div>
</div>
<div class="grid">
  <div class="card"><h3>技能偏好 Top10</h3><canvas id="skillChart"></canvas></div>
  <div class="card"><h3>月度工作量</h3><canvas id="monthChart"></canvas></div>
</div>
<script>
const skillData = {skill_top10};
new Chart(document.getElementById('skillChart'), {{type:'bar',data:{{labels:skillData.map(d=>d.x),datasets:[{{label:'调用次数',data:skillData.map(d=>d.y)}}]}},options:{{responsive:true}}}});
</script>
</body>
</html>'''

    # 简化版 HTML:完整版应包含所有 6 张图表和任务表
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(html)
    return output_path


# ---------------------------- 主流程 ----------------------------
def main():
    print("[1/5] 提取任务基础信息...")
    tasks = load_tasks()
    print(f"      共 {len(tasks)} 个任务")

    print("[2/5] 提取产物文件...")
    for t in tasks:
        t['products'] = get_products(t['id'])

    print("[3/5] 提取技能调用...")
    for t in tasks:
        t['skills'] = get_skills(t['id'])

    print("[4/5] 生成 Excel...")
    xlsx_path = safe_write_xlsx(OUTPUT_XLSX, lambda p: generate_excel(tasks, p))
    print(f"      已保存: {xlsx_path}")

    print("[5/5] 生成 HTML...")
    html_path = generate_html(tasks, OUTPUT_HTML)
    print(f"      已保存: {html_path}")

    # 输出关键摘要
    skill_counter = Counter()
    for t in tasks:
        for s in t['skills']:
            skill_counter[s] += 1
    top3 = skill_counter.most_common(3)
    print("\n关键摘要:")
    print(f"  - 总任务: {len(tasks)}")
    print(f"  - 总积分: {sum(t['credits'] for t in tasks):,}")
    print(f"  - Top3 技能: {', '.join(get_display_name(s) + f'({c})' for s, c in top3)}")


def safe_write_xlsx(output_path, writer_func, max_retries=3):
    """安全写入 Excel,遇文件锁定自动加后缀重试"""
    for i in range(max_retries + 1):
        try:
            base, ext = os.path.splitext(output_path)
            path = output_path if i == 0 else f"{base}_v{i}{ext}"
            writer_func(path)
            return path
        except PermissionError:
            if i < max_retries:
                print(f"      文件被占用,尝试保存为 {base}_v{i+1}{ext}")
                continue
            raise


if __name__ == '__main__':
    main()

运行方式

  1. 复制上方代码保存为 generate_workbuddy_report.py
  2. 在隔离 venv 中运行:
    C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\python.exe generate_workbuddy_report.py
  3. 当前目录会生成 WorkBuddy任务统计.xlsx 和 WorkBuddy任务统计看板.html

Token 与模型拆解脚本(scripts/ 目录)

本技能 v5.0 起附带两个独立脚本,用于 Token 用量结构分析(详见「第七章」):

# 1) 全局 token 结构 + 大模型涨价影响测算器
C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\python.exe scripts/token_structure_report.py
#   → 生成 WorkBuddy_token结构看板.html

# 2) 各模型双维度拆解(用户入口 × 真实底座)+ 交叉验证矩阵
C:\Users\<用户>\.workbuddy\binaries\python\envs\default\Scripts\python.exe scripts/model_token_report_v2.py
#   → 生成 WorkBuddy_模型token看板_v2.html

两个脚本仅依赖 Python 标准库(json/re/sqlite3/os),无需 openpyxl;产物 HTML 输出到运行时的当前目录。口径与交叉验证逻辑见第七章,已实测校准。


反馈与贡献

本技能由作者持续维护,欢迎反馈使用问题或提出新需求,以便及时迭代升级。

  • 微信公众号:AI重构咨询的李军师(在微信内搜索关注后留言,或于该号后台发送消息)
  • 邮箱:Ai_lichanghong@163.com

反馈时请尽量附上:使用的 WorkBuddy 版本、遇到的现象或报错截图、期望的效果。作者收到后会尽快处理并发布新版。

文档版本:v5.0.0 | 最后更新:2026-08-25

常见问题

5 问
答案由本站依据条目字段整理
WorkBuddy 使用报告怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_d8cafa77/workbuddy-usage-report。安装方式为未标注。装完刷新技能列表或重开客户端。

WorkBuddy 使用报告支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

WorkBuddy 使用报告权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

WorkBuddy 使用报告收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

WorkBuddy 使用报告用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。