1. 项目概述与核心价值

最近在深度使用一些AI辅助编程工具,比如Cursor和Claude,发现一个挺实际的问题:你很难直观地知道这些工具到底“吃”了你多少算力,或者说,花了你多少钱。尤其是当你在一个项目里频繁调用不同模型(比如Claude 3.5 Sonnet, GPT-4o, DeepSeek Coder)时,账单明细往往藏在后台日志里,不翻个底朝天根本看不清。这种“黑盒”体验对于想控制成本、或者单纯想了解自己工作习惯的开发者来说,并不友好。

于是,我动手搓了一个本地化的Token用量监控面板,我把它叫做 Token Usage UI 。本质上,它是一个运行在你本地的轻量级Web仪表盘,专门用来解析、聚合并可视化你的AI工具(目前主要适配OpenClaw框架)的会话数据。它能清晰地告诉你:过去一段时间,你在哪些模型上花了多少Token,对应的成本是多少,以及用量随时间的变化趋势。整个工具完全本地运行,不依赖任何外部服务,数据隐私有保障,启动也只需要一条命令。

如果你也经常用Cursor、Claude Code或者基于OpenClaw的自定义工作流,并且对“用量”和“成本”这两个词敏感,那么这个工具应该能帮到你。它特别适合独立开发者、小团队或者任何想精细化运营自己AI开发工具链的人。接下来,我会详细拆解这个工具的设计思路、实现细节,并分享我在开发过程中踩过的坑和总结的经验。

2. 整体架构与设计思路拆解

2.1 核心需求与方案选型

做这个工具的初衷很明确: 可视化、本地化、零依赖 。我不想引入复杂的后端框架、数据库,甚至不想用 pip install 装一堆包。目标就是写一个脚本,双击(或一条命令)就能跑起来,在浏览器里看到一个清晰的数据面板。

基于这个目标,技术选型就非常直接了:

  1. 数据源 :工具必须能直接读取AI工具产生的原生日志或会话文件。对于OpenClaw,其会话数据以JSON格式存储在 ~/.openclaw/agents/main/sessions/sessions.json 这个固定路径。这是最可靠的一手数据源。
  2. 后端服务 :为了极致轻量,我选择了Python标准库中的 http.server 模块。它虽然功能基础,但足以胜任一个静态文件服务器加上简单的API端点(用于提供动态数据)的任务。这意味着你只需要有Python环境,无需安装任何第三方库(如Flask, FastAPI)。
  3. 前端呈现 :仪表盘需要图表。为了保持“零依赖”且便于部署,我选择了通过CDN引入 Chart.js 。这是一个功能强大且易于使用的JavaScript图表库,画个折线图、柱状图绰绰有余。页面样式则用纯CSS实现,为了阅读舒适,采用了深色主题和响应式布局,并引入了Google Fonts的Inter字体提升视觉质感。
  4. 数据兜底 :考虑到用户可能第一次使用,或者 sessions.json 文件还不存在,工具必须能优雅降级。我设计了一个 Mock数据生成器 ,当检测不到真实数据时,会自动生成一份结构相同但数据为模拟值的JSON,确保前端页面永远有数据可展示,不会白屏报错。

这个架构的优点是 极其简单和便携 。整个项目就是一个文件夹,里面一个Python服务器脚本、一个HTML前端文件、一点CSS和JS。复制到任何有Python的机器上都能运行。缺点是功能扩展性有限,比如无法做复杂的历史数据查询或用户管理,但对于个人监控场景,这完全够用。

2.2 数据流与核心模块设计

整个工具的数据流非常清晰,可以分为三个核心模块:

  1. 数据采集与解析模块 :这是后端的核心。 server.py 中的一个函数会定期(或根据请求)去读取 ~/.openclaw/agents/main/sessions/sessions.json 文件。这个JSON文件通常是一个数组,里面的每个对象代表一次AI会话,包含了 model_name (模型名称)、 input_tokens (输入token数)、 output_tokens (输出token数)、 timestamp (时间戳)等关键字段。解析器需要遍历所有会话,按模型进行聚合统计。
  2. 数据聚合与成本计算模块 :解析出原始数据后,不能直接扔给前端。这里需要进行关键的计算:
    • 按模型聚合 :将属于同一个模型(如 claude-3-5-sonnet-20241022 )的所有会话的输入、输出token分别累加。
    • 成本计算 :这是最有价值的部分。我需要维护一个内部的 模型单价字典 。例如,我知道Claude 3.5 Sonnet的输入Token单价可能是 $0.003 / 1K tokens ,输出是 $0.015 / 1K tokens 。那么,成本 = (输入Token数 / 1000 * 输入单价) + (输出Token数 / 1000 * 输出单价)。所有模型的成本相加,就是总成本。这个单价字典需要我根据各AI服务商公开的定价信息手动维护和更新。
    • 时间线聚合 :为了绘制7天趋势图,需要按天对总Token数(输入+输出)进行分组求和。
  3. HTTP服务与前端渲染模块 :Python的HTTP服务器会做两件事:一是托管静态文件(HTML, CSS, JS),二是提供一个特定的API路由(比如 /api/usage )。当浏览器打开页面时,前端JavaScript会调用这个API获取上面计算好的聚合数据(JSON格式),然后利用Chart.js将数据渲染成图表和数字面板。

注意 :模型单价是成本计算的核心,但也是需要手动维护的部分。不同服务商、不同模型、甚至不同区域的单价都可能不同。在我的实现中,我将这个单价字典直接硬编码在 server.py 里,这对于个人使用是没问题的。如果你需要更灵活的配置,可以考虑将其移到一个外部的JSON配置文件中。

3. 核心实现细节与实操要点

3.1 后端服务器 (server.py) 深度解析

server.py 是这个项目的大脑,它虽然代码量不大,但每一部分都承担着关键职责。我采用了一个继承自 http.server.SimpleHTTPRequestHandler 的自定义类,通过重写 do_GET 方法来处理不同的请求路径。

import http.server
import json
import os
from datetime import datetime, timedelta
import time

# 1. 定义模型单价(单位:美元/1K tokens)
# 这里需要你根据实际使用的API价格进行更新
MODEL_PRICES = {
    "claude-3-5-sonnet-20241022": {"input": 0.003, "output": 0.015},
    "gpt-4o": {"input": 0.005, "output": 0.015},
    "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
    "deepseek-coder": {"input": 0.00014, "output": 0.00028},
    # 可以继续添加其他模型
}

class TokenUsageHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        # 处理API数据请求
        if self.path == '/api/usage':
            data = self.get_usage_data()
            self.send_response(200)
            self.send_header('Content-type', 'application/json')
            self.send_header('Access-Control-Allow-Origin', '*') # 简单处理CORS
            self.end_headers()
            self.wfile.write(json.dumps(data).encode())
        # 处理前端页面请求
        elif self.path == '/':
            self.path = '/index.html' # 将根路径重定向到首页
            return super().do_GET()
        # 处理其他静态文件(CSS, JS, 图片等)
        else:
            return super().do_GET()

    def get_usage_data(self):
        # 核心数据获取与计算函数
        sessions_file = os.path.expanduser('~/.openclaw/agents/main/sessions/sessions.json')
        sessions = []
        
        # 尝试读取真实数据
        if os.path.exists(sessions_file):
            try:
                with open(sessions_file, 'r') as f:
                    sessions = json.load(f)
                if not isinstance(sessions, list):
                    sessions = [] # 如果文件格式不对,回退到模拟数据
            except (json.JSONDecodeError, IOError):
                sessions = [] # 文件损坏或读取失败,回退到模拟数据
        
        # 如果真实数据为空,生成模拟数据
        if not sessions:
            sessions = self.generate_mock_sessions()
        
        # 开始核心计算逻辑...
        # ... (后续聚合计算代码)

关键点解析:

  1. 路由分发 :在 do_GET 方法中,我根据 self.path 判断请求类型。如果是 /api/usage ,就执行数据计算并返回JSON;如果是 / ,就返回 index.html ;其他情况则交给父类处理静态文件。这种设计让一个简单的HTTP服务器同时具备了API和静态资源服务的能力。
  2. 数据读取与兜底 get_usage_data 函数首先尝试从固定路径读取 sessions.json 。这里使用了 os.path.expanduser 来正确处理 ~ 符号,保证在不同系统上都能找到用户目录。读取时加了 try...except 异常捕获,防止因为文件格式错误导致整个服务崩溃。如果读取失败或文件为空,则调用 generate_mock_sessions 生成模拟数据。 这是一个非常重要的健壮性设计 ,确保了工具在任何情况下都能“跑起来”。
  3. 模拟数据生成 generate_mock_sessions 函数会生成过去7天内、不同模型的随机会话数据。这不仅用于兜底,在开发测试阶段也极其有用,让你在没有真实数据的情况下也能看到完整的UI效果。

3.2 数据聚合与成本计算逻辑

这是后端最核心的算法部分,直接决定了仪表盘上数字的准确性。

    def get_usage_data(self):
        # ... (前述数据读取部分)
        
        # 初始化统计数据结构
        model_breakdown = {}
        total_input_tokens = 0
        total_output_tokens = 0
        total_cost = 0.0
        
        # 按模型聚合
        for session in sessions:
            model = session.get('model_name', 'unknown')
            input_tokens = session.get('input_tokens', 0)
            output_tokens = session.get('output_tokens', 0)
            timestamp = session.get('timestamp')
            
            # 初始化该模型的统计项
            if model not in model_breakdown:
                model_breakdown[model] = {
                    'input_tokens': 0,
                    'output_tokens': 0,
                    'cost': 0.0,
                    'session_count': 0
                }
            
            # 累加Token数
            model_breakdown[model]['input_tokens'] += input_tokens
            model_breakdown[model]['output_tokens'] += output_tokens
            model_breakdown[model]['session_count'] += 1
            
            # 计算该会话成本并累加
            cost = self.calculate_session_cost(model, input_tokens, output_tokens)
            model_breakdown[model]['cost'] += cost
            
            # 累加到总计
            total_input_tokens += input_tokens
            total_output_tokens += output_tokens
            total_cost += cost
        
        # 准备7天时间线数据
        timeline_data = self.aggregate_timeline_data(sessions)
        
        # 构建最终返回给前端的JSON
        result = {
            'total': {
                'input_tokens': total_input_tokens,
                'output_tokens': total_output_tokens,
                'cost': round(total_cost, 4) # 保留4位小数
            },
            'models': model_breakdown,
            'timeline': timeline_data
        }
        return result
    
    def calculate_session_cost(self, model, input_tokens, output_tokens):
        """根据模型和Token数计算单次会话成本"""
        if model not in MODEL_PRICES:
            return 0.0 # 未知模型,成本记为0
        
        price = MODEL_PRICES[model]
        # 成本 = (输入Token/1000 * 输入单价) + (输出Token/1000 * 输出单价)
        input_cost = (input_tokens / 1000.0) * price['input']
        output_cost = (output_tokens / 1000.0) * price['output']
        return input_cost + output_cost
    
    def aggregate_timeline_data(self, sessions):
        """聚合过去7天每天的Token总量"""
        timeline = {}
        today = datetime.now().date()
        
        # 初始化过去7天的日期字典,Token数默认为0
        for i in range(6, -1, -1): # 从6天前到今天
            date_str = str(today - timedelta(days=i))
            timeline[date_str] = 0
        
        # 遍历所有会话,按日期累加
        for session in sessions:
            ts = session.get('timestamp')
            if not ts:
                continue
            # 假设timestamp是ISO格式字符串或时间戳
            try:
                if isinstance(ts, (int, float)):
                    dt = datetime.fromtimestamp(ts)
                else:
                    dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
                date_str = str(dt.date())
                # 只统计最近7天的数据
                if date_str in timeline:
                    total_tokens = session.get('input_tokens', 0) + session.get('output_tokens', 0)
                    timeline[date_str] += total_tokens
            except (ValueError, TypeError):
                continue # 时间戳格式错误,跳过
        
        # 转换为前端需要的数组格式:[{date: '2024-01-01', tokens: 1234}, ...]
        timeline_list = [{'date': date, 'tokens': tokens} for date, tokens in timeline.items()]
        return timeline_list

计算逻辑要点:

  1. 模型聚合 :使用一个字典 model_breakdown 来为每个遇到的模型维护一个统计对象。遍历所有会话时,将Token数和会话次数累加到对应的模型下。这种“字典归类”的方法是处理此类聚合问题的标准做法,时间复杂度是O(n),效率很高。
  2. 成本计算 calculate_session_cost 函数是核心。它严格遵循 成本 = (Token数 / 1000) * 单价 的公式。这里 必须注意单位的统一 :我的 MODEL_PRICES 字典里单价单位是“美元/1K tokens”,所以计算时要先将Token数除以1000。另外,对于未在价格字典中定义的模型,我选择返回0成本,避免因价格缺失导致整个计算出错。在实际使用中,你需要定期检查并更新这个价格字典。
  3. 时间线聚合 aggregate_timeline_data 函数负责生成趋势图所需的数据。我首先初始化了一个包含过去7天日期的字典,值都为0。然后遍历会话,解析其时间戳,如果日期落在最近7天内,就将该会话的总Token数累加到对应的日期上。最后,将字典转换为一个按日期排序的列表,方便前端Chart.js使用。这里的时间戳解析需要一定的容错处理,因为数据源格式可能不统一。

实操心得 :在计算成本时,浮点数精度是个小坑。美元金额通常只需要保留小数点后2-4位。我使用 round(total_cost, 4) 来格式化最终的总成本。但在内部累加时,应保持原始精度,只在最终展示时进行舍入,避免误差累积。

3.3 前端界面 (index.html) 与动态数据绑定

前端页面是一个单页应用,核心是使用JavaScript调用后端API,获取数据后动态更新DOM和图表。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Token Usage Dashboard</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        /* 深色主题CSS样式 */
        :root { --bg-primary: #0f172a; --bg-card: #1e293b; --text-primary: #f1f5f9; ... }
        body { font-family: 'Inter', sans-serif; background: var(--bg-primary); color: var(--text-primary); ... }
        .card { background: var(--bg-card); border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
        .stat-number { font-size: 2.5rem; font-weight: 700; color: #60a5fa; }
        /* 更多响应式样式... */
    </style>
</head>
<body>
    <div class="container">
        <header>...</header>
        
        <section class="card">
            <h2>📊 Total Usage & Cost</h2>
            <div class="stats-grid">
                <div class="stat">
                    <div class="stat-label">Total Input Tokens</div>
                    <div id="total-input" class="stat-number">0</div>
                </div>
                <!-- 其他统计项:Total Output, Total Cost -->
            </div>
        </section>

        <section class="card">
            <h2>🧩 Per-Model Breakdown</h2>
            <div id="model-breakdown">
                <!-- 这里会被JS动态填充 -->
            </div>
        </section>

        <section class="card">
            <h2>📈 7-Day Token Usage Timeline</h2>
            <canvas id="timeline-chart"></canvas>
        </section>
    </div>

    <script>
        let timelineChart = null; // 保存图表实例

        // 页面加载完成后执行
        document.addEventListener('DOMContentLoaded', function() {
            fetchUsageData(); // 首次加载数据
            setInterval(fetchUsageData, 8000); // 每8秒自动刷新
        });

        async function fetchUsageData() {
            try {
                const response = await fetch('/api/usage');
                const data = await response.json();
                updateDashboard(data);
            } catch (error) {
                console.error('Failed to fetch usage data:', error);
                // 可以在这里添加一个友好的错误提示到页面上
            }
        }

        function updateDashboard(data) {
            // 1. 更新总计面板
            document.getElementById('total-input').textContent = data.total.input_tokens.toLocaleString();
            document.getElementById('total-output').textContent = data.total.output_tokens.toLocaleString();
            document.getElementById('total-cost').textContent = '$' + data.total.cost.toFixed(4);
            
            // 2. 更新模型细分表格
            updateModelBreakdown(data.models);
            
            // 3. 更新趋势图
            updateTimelineChart(data.timeline);
        }

        function updateModelBreakdown(models) {
            const container = document.getElementById('model-breakdown');
            let html = '<table class="model-table"><thead><tr><th>Model</th><th>Input Tokens</th><th>Output Tokens</th><th>Cost</th><th>Sessions</th></tr></thead><tbody>';
            
            for (const [modelName, stats] of Object.entries(models)) {
                html += `
                <tr>
                    <td><strong>${modelName}</strong></td>
                    <td>${stats.input_tokens.toLocaleString()}</td>
                    <td>${stats.output_tokens.toLocaleString()}</td>
                    <td>$${stats.cost.toFixed(4)}</td>
                    <td>${stats.session_count}</td>
                </tr>`;
            }
            
            html += '</tbody></table>';
            container.innerHTML = html;
        }

        function updateTimelineChart(timelineData) {
            const ctx = document.getElementById('timeline-chart').getContext('2d');
            const dates = timelineData.map(d => d.date);
            const tokenCounts = timelineData.map(d => d.tokens);
            
            // 如果图表已存在,则更新数据;否则创建新图表
            if (timelineChart) {
                timelineChart.data.labels = dates;
                timelineChart.data.datasets[0].data = tokenCounts;
                timelineChart.update();
            } else {
                timelineChart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: dates,
                        datasets: [{
                            label: 'Total Tokens (Input+Output)',
                            data: tokenCounts,
                            borderColor: '#60a5fa',
                            backgroundColor: 'rgba(96, 165, 250, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4 // 让折线更平滑
                        }]
                    },
                    options: {
                        responsive: true,
                        plugins: { legend: { display: true } },
                        scales: {
                            y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' } },
                            x: { grid: { color: 'rgba(255,255,255,0.1)' } }
                        }
                    }
                });
            }
        }
    </script>
</body>
</html>

前端实现要点:

  1. 自动刷新 :通过 setInterval(fetchUsageData, 8000) 实现了每8秒自动获取最新数据并更新页面。这个间隔时间可以根据需要调整,太短会增加服务器压力,太长则数据不够实时。
  2. 数据绑定 updateDashboard 函数是中枢,它接收到后端传来的JSON数据后,分发给三个子函数去更新页面的不同部分。这种“数据驱动视图”的模式非常清晰。
  3. 模型细分表格 updateModelBreakdown 函数动态生成一个HTML表格。这里我使用了模板字符串来拼接HTML,代码更清晰。注意使用 .toLocaleString() 为数字添加千位分隔符,使用 .toFixed(4) 控制成本显示的小数位数,这些都是提升可读性的细节。
  4. Chart.js 图表 updateTimelineChart 函数处理折线图。关键点在于,第一次调用时创建图表实例并保存到 timelineChart 变量中,后续调用时只需更新该实例的 data 属性并调用 update() 方法,这样图表就会平滑过渡到新数据,而不是重新创建整个图表,体验更好。

注意事项 :前端代码直接内嵌在HTML中,没有使用任何构建工具,这保持了项目的简洁性。但在实际开发中,如果逻辑变复杂,可以考虑将CSS和JS拆分到独立文件中。另外,Chart.js通过CDN引入,这意味着使用该工具时需要网络连接。如果需要在完全离线的环境使用,可以将Chart.js库文件下载到本地并修改引用路径。

4. 部署、运行与自定义指南

4.1 环境准备与一键启动

这个工具对环境的要求极低,只需要一个 Python 3.6+ 的运行环境。无需虚拟环境,无需安装任何第三方包。

  1. 获取代码 :你可以直接从GitHub仓库克隆,或者手动创建项目文件。

    # 假设你克隆到了本地
    cd /path/to/token-usage-ui
    

    项目目录结构应如下所示:

    token-usage-ui/
    ├── server.py          # Python后端服务器
    ├── index.html         # 前端主页面
    └── (可能还有 style.css, 但本例中样式内联了)
    
  2. 启动服务器 :在项目根目录下,运行:

    python server.py
    

    默认情况下,服务器会监听本地的 8765 端口。你会看到类似下面的输出:

    Serving HTTP on 0.0.0.0 port 8765 (http://0.0.0.0:8765/) ...
    
  3. 访问仪表盘 :打开你的浏览器,输入 http://localhost:8765 ,就能看到Token用量监控面板了。页面会每8秒自动刷新一次数据。

4.2 关键配置与自定义点

虽然工具开箱即用,但有几个地方你可能需要根据自身情况调整:

  1. 修改监听端口 :如果8765端口被占用,你可以在 server.py 文件末尾修改端口号。

    if __name__ == '__main__':
        server_address = ('', 8765)  # 将8765改为其他端口,如8080
        httpd = http.server.HTTPServer(server_address, TokenUsageHandler)
        print(f'Starting server on port {server_address[1]}...')
        httpd.serve_forever()
    
  2. 更新模型单价 :这是保证成本计算准确的关键。你需要编辑 server.py 文件开头的 MODEL_PRICES 字典。

    MODEL_PRICES = {
        "claude-3-5-sonnet-20241022": {"input": 0.003, "output": 0.015},
        "gpt-4o": {"input": 0.005, "output": 0.015},
        # 访问OpenAI、Anthropic、DeepSeek等官网的Pricing页面,获取最新价格并更新此处。
        # 注意单位是 美元/1K tokens。
    }
    

    重要 :模型名称必须与 sessions.json 文件中的 model_name 字段完全一致,包括大小写和版本号,否则会被识别为“未知模型”,成本计算为0。

  3. 调整数据源路径 :如果你的OpenClaw会话文件不在默认路径,或者你想监控其他工具生成的日志,需要修改 get_usage_data 函数中的 sessions_file 变量。

    sessions_file = '/your/custom/path/to/sessions.json'  # 修改为你的实际路径
    

    只要你的JSON文件格式与OpenClaw的 sessions.json 兼容(即一个会话对象数组,包含 model_name , input_tokens , output_tokens , timestamp 字段),工具就能正常解析。

  4. 修改自动刷新频率 :前端页面默认8秒刷新一次。如果你觉得太频繁或太慢,可以编辑 index.html 文件中的JavaScript部分。

    setInterval(fetchUsageData, 8000); // 将8000(毫秒)改为你想要的间隔,如15000(15秒)
    

4.3 适配其他AI工具的数据源

这个工具的核心是解析固定的JSON数据格式。如果你想用它来监控非OpenClaw的工具(比如你自己写的一个脚本,调用了OpenAI API),你需要确保你的工具能以类似的格式输出日志。

一个简单的日志生成示例(Python):

import json
import time

def log_ai_session(model, input_tokens, output_tokens, cost=None):
    log_entry = {
        "model_name": model,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "timestamp": time.time(), # 或者 datetime.now().isoformat()
        # 可以添加其他自定义字段,如"project", "user"等
    }
    
    log_file = "~/my_ai_sessions.json"
    # 读取现有日志
    try:
        with open(os.path.expanduser(log_file), 'r') as f:
            sessions = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        sessions = []
    
    # 追加新日志
    sessions.append(log_entry)
    
    # 写回文件(注意:这不是线程安全的,生产环境需加锁或使用数据库)
    with open(os.path.expanduser(log_file), 'w') as f:
        json.dump(sessions, f, indent=2)

然后,将 server.py 中的数据源路径指向这个新的JSON文件即可。通过这种方式,你可以将任何产生Token用量的服务接入到这个监控面板中。

5. 常见问题排查与优化技巧

在实际使用和开发过程中,我遇到了一些典型问题,这里总结出来,方便你快速排查。

5.1 问题排查速查表

问题现象 可能原因 解决方案
访问 http://localhost:8765 显示“无法连接”或“拒绝连接”。 1. server.py 没有运行。
2. 端口被其他程序占用。
3. 防火墙阻止了该端口。
1. 在终端确认 python server.py 正在运行且无报错。
2. 尝试更换端口(如8080),并访问 http://localhost:8080
3. 检查本地防火墙设置,暂时允许该端口的入站连接。
页面能打开,但所有数据都显示为0或“No Data”。 1. 数据源路径 ( sessions.json ) 不正确或文件不存在。
2. JSON文件格式错误,无法解析。
3. 文件权限不足,无法读取。
1. 检查 server.py 中的 sessions_file 路径是否正确。使用 print 语句输出该路径确认。
2. 手动打开 sessions.json 文件,检查是否是有效的JSON数组格式。可以使用在线JSON校验工具。
3. 检查文件读权限 ( ls -l ~/.openclaw/.../sessions.json )。
成本计算为0,但Token数有显示。 1. MODEL_PRICES 字典中没有匹配当前 model_name 的键。
2. 模型名称字符串有细微差别(如尾部空格、版本号不同)。
1. 在页面表格中查看具体的模型名称,然后将其添加到 MODEL_PRICES 字典中,并设置正确的单价。
2. 仔细核对模型名称,确保完全一致。可以在 server.py 中添加 print(model) 来调试。
图表不显示或显示错误。 1. 网络问题导致Chart.js CDN加载失败。
2. 传递给Chart.js的数据格式不正确。
3. Canvas上下文获取失败。
1. 检查浏览器开发者工具(F12)的“网络(Network)”标签,看 chart.js 是否加载成功。失败可尝试使用本地Chart.js文件。
2. 在 updateTimelineChart 函数中 console.log(dates, tokenCounts) 检查数据格式。
3. 确认HTML中 <canvas id="timeline-chart"> 的ID与JavaScript中 getElementById 的ID一致。
页面样式混乱或字体未加载。 1. 网络问题导致Google Fonts (Inter) 加载失败。
2. CSS代码有语法错误。
1. 同样在开发者工具的“网络”标签中检查字体加载情况。可以考虑将Inter字体下载到本地,或使用系统备用字体(如 font-family: Inter, -apple-system, sans-serif; )。
2. 检查内联CSS的语法,确保括号闭合。

5.2 性能与稳定性优化建议

  1. 会话文件过大 :如果长期使用, sessions.json 文件可能会变得非常大(几十MB甚至更大)。每次请求都读取和解析整个大文件会严重影响服务器响应速度。

    • 优化方案 :可以在 server.py 中增加缓存机制。例如,将解析后的数据缓存在内存中,并设置一个过期时间(如5秒)。只有在缓存过期或检测到文件修改时间变化时,才重新读取和解析文件。这能极大减少IO操作。
    import os
    import time
    
    class TokenUsageHandler(...):
        _data_cache = None
        _cache_time = 0
        _file_mtime = 0
        
        def get_usage_data(self):
            current_time = time.time()
            sessions_file = '...'
            # 获取文件最后修改时间
            try:
                file_mtime = os.path.getmtime(sessions_file)
            except OSError:
                file_mtime = 0
                
            # 如果缓存未过期(比如5秒内)且文件未修改,则返回缓存
            if (self._data_cache is not None and 
                current_time - self._cache_time < 5 and 
                self._file_mtime == file_mtime):
                return self._data_cache
                
            # 否则,重新计算数据
            data = ... # 原有的计算逻辑
            self._data_cache = data
            self._cache_time = current_time
            self._file_mtime = file_mtime
            return data
    
  2. 数据安全性 :这个工具默认监听 0.0.0.0:8765 ,意味着同一网络下的其他设备可能也能访问你的仪表盘。

    • 建议 :如果只在本地使用,可以将服务器地址改为 127.0.0.1 (本地回环地址),这样只有本机可以访问。
    server_address = ('127.0.0.1', 8765)  # 只绑定到本地
    
  3. 扩展更多图表 :目前只展示了7天的总Token趋势图。你可以很容易地扩展更多视图。

    • 例如 :增加一个“模型成本占比”饼图。在后端 get_usage_data 函数中,计算每个模型的成本占总成本的比例。然后在前端用Chart.js的 doughnut pie 类型图表进行渲染。
    • 例如 :增加一个“每小时用量热力图”,展示一天中哪些时段使用AI最频繁。这需要你的原始数据中包含更精确的时间戳。

5.3 从“能用”到“好用”的打磨

  1. 美化前端 :目前的UI比较基础。你可以通过CSS加入更多现代设计元素,比如卡片阴影、悬停效果、渐变色、更优雅的字体排版等。也可以使用一些轻量级的CSS框架(如Pico.css)快速提升颜值。

  2. 添加数据过滤 :在界面上增加日期选择器或模型筛选器,让用户可以查看特定时间段或特定模型的数据。这需要后端API支持查询参数,例如 /api/usage?start_date=2024-01-01&model=claude-3-5-sonnet

  3. 数据持久化与历史对比 :当前工具只展示实时数据,没有历史趋势对比。你可以修改后端,定期(如每天)将聚合结果保存到一个单独的JSON或SQLite数据库中。然后前端可以提供一个下拉菜单,让用户选择查看“今天”、“昨天”、“上周同期”或“自定义日期范围”的数据对比。

  4. 成本预警 :在后端逻辑中加入成本检查。可以设置一个每日或每周的预算阈值(如10美元),当计算出的成本超过阈值时,在返回的数据中添加一个警告标志,前端收到后可以在页面上显示一个明显的警告横幅或发送浏览器通知。

这个工具麻雀虽小,五脏俱全。它从一个具体的痛点出发,用最简单的技术栈实现了一个可用的解决方案。更重要的是,它的架构是清晰的,你可以基于它,根据自己的需求进行无限的定制和扩展。无论是想监控成本,还是单纯想了解自己的工作模式,这样一个透明的数据面板都能给你带来更踏实的掌控感。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐