myccusage 1.2.1__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {myccusage-1.2.1 → myccusage-1.2.2}/PKG-INFO +1 -1
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage.egg-info/PKG-INFO +1 -1
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/__init__.py +1 -1
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/core.py +105 -78
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/web/static/app.js +22 -10
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/web/static/index.html +1 -1
- {myccusage-1.2.1 → myccusage-1.2.2}/pyproject.toml +1 -1
- {myccusage-1.2.1 → myccusage-1.2.2}/LICENSE +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/README.md +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage.egg-info/SOURCES.txt +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage.egg-info/dependency_links.txt +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage.egg-info/entry_points.txt +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage.egg-info/top_level.txt +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/cli.py +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/web/__init__.py +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/web/server.py +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/myccusage_lib/web/static/style.css +0 -0
- {myccusage-1.2.1 → myccusage-1.2.2}/setup.cfg +0 -0
|
@@ -14,6 +14,8 @@ import glob
|
|
|
14
14
|
import sys
|
|
15
15
|
import sqlite3
|
|
16
16
|
import os
|
|
17
|
+
import time
|
|
18
|
+
import threading
|
|
17
19
|
import shutil
|
|
18
20
|
import concurrent.futures
|
|
19
21
|
from datetime import datetime, timezone
|
|
@@ -21,6 +23,16 @@ from collections import OrderedDict
|
|
|
21
23
|
|
|
22
24
|
WEEKDAYS = ["一", "二", "三", "四", "五", "六", "日"]
|
|
23
25
|
|
|
26
|
+
_AGENT_LOCKS = {}
|
|
27
|
+
_AGENT_LOCKS_MUTEX = threading.Lock()
|
|
28
|
+
|
|
29
|
+
def get_agent_lock(agent_type):
|
|
30
|
+
"""获取指定 Agent 专用的互斥锁,避免同一 Agent 多个进程并发竞争本地 SQLite 数据库"""
|
|
31
|
+
with _AGENT_LOCKS_MUTEX:
|
|
32
|
+
if agent_type not in _AGENT_LOCKS:
|
|
33
|
+
_AGENT_LOCKS[agent_type] = threading.Lock()
|
|
34
|
+
return _AGENT_LOCKS[agent_type]
|
|
35
|
+
|
|
24
36
|
SUPPORTED_AGENTS = {
|
|
25
37
|
"agy": {"name": "Google Antigravity", "subcmd": "antigravity", "has_times": False},
|
|
26
38
|
"claude": {"name": "Claude Code", "subcmd": "claude", "has_times": False},
|
|
@@ -388,28 +400,43 @@ def check_ccusage_installed():
|
|
|
388
400
|
"安装完成后重新运行当前命令即可。"
|
|
389
401
|
)
|
|
390
402
|
|
|
391
|
-
def run_ccusage(args, capture_output=True, text=True):
|
|
403
|
+
def run_ccusage(args, capture_output=True, text=True, max_retries=3):
|
|
392
404
|
"""
|
|
393
405
|
统一安全调用底层 ccusage CLI:
|
|
394
406
|
- 优先追加 --offline 参数阻断冗余公网 LiteLLM 模型价格拉取 (提速 10x+)
|
|
407
|
+
- 若遇到 database is locked 错误,支持毫秒级退避重试 (解决 SQLite 读写瞬态争抢)
|
|
395
408
|
- 若旧版本不支持 --offline 则自动优雅降级回退执行
|
|
396
409
|
"""
|
|
397
410
|
check_ccusage_installed()
|
|
398
411
|
cmd = ["ccusage"] + list(args)
|
|
399
412
|
if "--offline" not in cmd:
|
|
400
413
|
cmd.append("--offline")
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
414
|
+
|
|
415
|
+
for attempt in range(max_retries):
|
|
416
|
+
res = subprocess.run(cmd, capture_output=capture_output, text=text)
|
|
417
|
+
stderr_lower = (res.stderr or "").lower()
|
|
418
|
+
|
|
419
|
+
# 检查是否为 SQLite 瞬态锁冲突,毫秒级退避重试
|
|
420
|
+
if res.returncode != 0 and ("database is locked" in stderr_lower or "code 5" in stderr_lower):
|
|
421
|
+
if attempt < max_retries - 1:
|
|
422
|
+
time.sleep(0.3 * (attempt + 1))
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
# 检查是否为旧版本不识别 --offline
|
|
426
|
+
if res.returncode != 0 and ("unknown" in stderr_lower or "unexpected" in stderr_lower):
|
|
427
|
+
cmd_fallback = [arg for arg in cmd if arg != "--offline"]
|
|
428
|
+
res = subprocess.run(cmd_fallback, capture_output=capture_output, text=text)
|
|
429
|
+
|
|
430
|
+
return res
|
|
431
|
+
|
|
406
432
|
return res
|
|
407
433
|
|
|
408
434
|
def fetch_single_day_sessions(ccusage_subcmd, date_str, times_override):
|
|
409
435
|
"""获取指定日期的精确切片会话消耗(不含历史前日累积)"""
|
|
410
436
|
res = run_ccusage([ccusage_subcmd, "session", "-s", date_str, "-u", date_str, "--json"])
|
|
411
437
|
if res.returncode != 0:
|
|
412
|
-
|
|
438
|
+
# 失败时返回 None 而非 [],严格区分“提取错误”与“该日无数据”,防止缓存污染
|
|
439
|
+
return None
|
|
413
440
|
try:
|
|
414
441
|
data = json.loads(res.stdout)
|
|
415
442
|
sessions = data.get("sessions", [])
|
|
@@ -419,7 +446,7 @@ def fetch_single_day_sessions(ccusage_subcmd, date_str, times_override):
|
|
|
419
446
|
s["lastActivity"] = times_override[sid]
|
|
420
447
|
return sessions
|
|
421
448
|
except Exception:
|
|
422
|
-
return
|
|
449
|
+
return None
|
|
423
450
|
|
|
424
451
|
def get_daily_data(agent_type, sort_by_tokens=False, force_refresh=False):
|
|
425
452
|
"""
|
|
@@ -434,71 +461,69 @@ def get_daily_data(agent_type, sort_by_tokens=False, force_refresh=False):
|
|
|
434
461
|
ccusage_subcmd = info["subcmd"]
|
|
435
462
|
titles, times_override = get_agent_metadata(agent_type)
|
|
436
463
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
464
|
+
agent_lock = get_agent_lock(agent_type)
|
|
465
|
+
with agent_lock:
|
|
466
|
+
# 1. 获取活动日基准 (带 --offline 阻断网络挂起)
|
|
467
|
+
res_daily = run_ccusage([ccusage_subcmd, "daily", "--json"])
|
|
468
|
+
if res_daily.returncode != 0:
|
|
469
|
+
raise RuntimeError(f"执行 ccusage {ccusage_subcmd} daily 失败: {res_daily.stderr}")
|
|
441
470
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
daily_list = daily_json.get("daily", [])
|
|
448
|
-
active_days = [d.get("date") for d in daily_list if d.get("date")]
|
|
449
|
-
active_days.sort()
|
|
471
|
+
try:
|
|
472
|
+
daily_json = json.loads(res_daily.stdout)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
raise RuntimeError(f"无法解析 ccusage {ccusage_subcmd} daily 输出: {e}")
|
|
450
475
|
|
|
451
|
-
|
|
476
|
+
daily_list = daily_json.get("daily", [])
|
|
477
|
+
active_days = [d.get("date") for d in daily_list if d.get("date")]
|
|
478
|
+
active_days.sort()
|
|
479
|
+
daily_tokens_map = {d.get("date"): d.get("totalTokens", 0) for d in daily_list if d.get("date")}
|
|
452
480
|
|
|
453
|
-
|
|
454
|
-
cache_dir = os.path.expanduser("~/.cache/myccusage")
|
|
455
|
-
os.makedirs(cache_dir, exist_ok=True)
|
|
456
|
-
cache_file = os.path.join(cache_dir, f"{agent_type}_daily.json")
|
|
481
|
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
457
482
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
cache = json.load(f)
|
|
463
|
-
except Exception:
|
|
464
|
-
cache = {}
|
|
465
|
-
|
|
466
|
-
cache_dirty = force_refresh
|
|
467
|
-
day_sessions_map = OrderedDict()
|
|
468
|
-
|
|
469
|
-
# 识别未在缓存中的历史天数 (若有则并发拉取,避免冷启动单线程逐天挂起)
|
|
470
|
-
uncached_history_days = [d for d in active_days if d != today_str and d not in cache]
|
|
471
|
-
if uncached_history_days:
|
|
472
|
-
max_workers = min(6, len(uncached_history_days))
|
|
473
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
474
|
-
future_to_day = {
|
|
475
|
-
pool.submit(fetch_single_day_sessions, ccusage_subcmd, d, times_override): d
|
|
476
|
-
for d in uncached_history_days
|
|
477
|
-
}
|
|
478
|
-
for fut in concurrent.futures.as_completed(future_to_day):
|
|
479
|
-
d = future_to_day[fut]
|
|
480
|
-
try:
|
|
481
|
-
cache[d] = fut.result()
|
|
482
|
-
cache_dirty = True
|
|
483
|
-
except Exception:
|
|
484
|
-
cache[d] = []
|
|
483
|
+
# 2. 读取/写入本地缓存
|
|
484
|
+
cache_dir = os.path.expanduser("~/.cache/myccusage")
|
|
485
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
486
|
+
cache_file = os.path.join(cache_dir, f"{agent_type}_daily.json")
|
|
485
487
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
488
|
+
cache = {}
|
|
489
|
+
if os.path.exists(cache_file):
|
|
490
|
+
try:
|
|
491
|
+
with open(cache_file, "r", encoding="utf-8") as f:
|
|
492
|
+
cache = json.load(f)
|
|
493
|
+
except Exception:
|
|
494
|
+
cache = {}
|
|
495
|
+
|
|
496
|
+
cache_dirty = False
|
|
497
|
+
day_sessions_map = OrderedDict()
|
|
498
|
+
|
|
499
|
+
# 识别缺失或先前被污染(有Token却被记录为空列表)的历史天数,顺序安全拉取修复
|
|
500
|
+
uncached_history_days = [
|
|
501
|
+
d for d in active_days
|
|
502
|
+
if d != today_str and (d not in cache or (daily_tokens_map.get(d, 0) > 0 and len(cache.get(d, [])) == 0))
|
|
503
|
+
]
|
|
504
|
+
for d in uncached_history_days:
|
|
505
|
+
s_list = fetch_single_day_sessions(ccusage_subcmd, d, times_override)
|
|
506
|
+
if s_list is not None:
|
|
507
|
+
cache[d] = s_list
|
|
508
|
+
cache_dirty = True
|
|
509
|
+
|
|
510
|
+
for d_str in active_days:
|
|
511
|
+
if d_str != today_str:
|
|
512
|
+
day_sessions_map[d_str] = cache.get(d_str, [])
|
|
513
|
+
else:
|
|
514
|
+
# 仅今日调用实时切片抓取
|
|
515
|
+
s_list = fetch_single_day_sessions(ccusage_subcmd, d_str, times_override)
|
|
516
|
+
if s_list is not None:
|
|
517
|
+
day_sessions_map[today_str] = s_list
|
|
518
|
+
else:
|
|
519
|
+
day_sessions_map[today_str] = cache.get(today_str, [])
|
|
520
|
+
|
|
521
|
+
if cache_dirty:
|
|
522
|
+
try:
|
|
523
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
524
|
+
json.dump(cache, f)
|
|
525
|
+
except Exception:
|
|
526
|
+
pass
|
|
502
527
|
|
|
503
528
|
# 3. 统计与分层聚合
|
|
504
529
|
grand_total = 0
|
|
@@ -664,18 +689,20 @@ def get_session_data(agent_type, sort_by_tokens=False, clean_args=None):
|
|
|
664
689
|
ccusage_subcmd = info["subcmd"]
|
|
665
690
|
titles, times_override = get_agent_metadata(agent_type)
|
|
666
691
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
sub_args
|
|
692
|
+
agent_lock = get_agent_lock(agent_type)
|
|
693
|
+
with agent_lock:
|
|
694
|
+
sub_args = [ccusage_subcmd, "session", "--json"]
|
|
695
|
+
if clean_args:
|
|
696
|
+
sub_args.extend(clean_args)
|
|
670
697
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
698
|
+
res = run_ccusage(sub_args)
|
|
699
|
+
if res.returncode != 0:
|
|
700
|
+
raise RuntimeError(f"执行 ccusage {ccusage_subcmd} session 失败: {res.stderr}")
|
|
674
701
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
702
|
+
try:
|
|
703
|
+
usage_data = json.loads(res.stdout)
|
|
704
|
+
except Exception as e:
|
|
705
|
+
raise RuntimeError(f"无法解析 ccusage {ccusage_subcmd} 输出: {e}")
|
|
679
706
|
|
|
680
707
|
raw_sessions = usage_data.get("sessions", [])
|
|
681
708
|
sessions = []
|
|
@@ -517,6 +517,19 @@
|
|
|
517
517
|
});
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
// 统一计价响应调度中心:模型切换或费率更新后,全局同步刷新当前激活视图的所有价格展示
|
|
521
|
+
function updateAllPricingDisplays() {
|
|
522
|
+
if (state.agent === 'all') {
|
|
523
|
+
if (state.allAgentsData) {
|
|
524
|
+
renderAllAgentsOverview(state.allAgentsData);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
if (state.data) {
|
|
528
|
+
renderDashboard(state.data);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
520
533
|
function setPricingModel(modelId, notify = false) {
|
|
521
534
|
const allModels = getAllPricingModels();
|
|
522
535
|
if (!allModels[modelId]) {
|
|
@@ -536,12 +549,8 @@
|
|
|
536
549
|
showToast(`已选择计价模型: ${model.name}`);
|
|
537
550
|
}
|
|
538
551
|
|
|
539
|
-
// 动态全站重算 (
|
|
540
|
-
|
|
541
|
-
renderDashboard(state.data);
|
|
542
|
-
} else if (state.allAgentsData) {
|
|
543
|
-
renderAllAgentsOverview(state.allAgentsData);
|
|
544
|
-
}
|
|
552
|
+
// 动态全站重算 (即时响应,调度计价中心)
|
|
553
|
+
updateAllPricingDisplays();
|
|
545
554
|
}
|
|
546
555
|
|
|
547
556
|
// ------------------------------------------------------------------------
|
|
@@ -687,8 +696,7 @@
|
|
|
687
696
|
|
|
688
697
|
if (state.pricingModel === currentId) {
|
|
689
698
|
el.selectedPricingName.textContent = updatedModel.name;
|
|
690
|
-
|
|
691
|
-
else if (state.allAgentsData) renderAllAgentsOverview(state.allAgentsData);
|
|
699
|
+
updateAllPricingDisplays();
|
|
692
700
|
}
|
|
693
701
|
|
|
694
702
|
showToast(`✅ 已保存模型修改: ${updatedModel.name}`);
|
|
@@ -756,8 +764,7 @@
|
|
|
756
764
|
const active = getActiveModel();
|
|
757
765
|
renderManagerDetail(active.id);
|
|
758
766
|
renderPricingDropdownMenu();
|
|
759
|
-
|
|
760
|
-
else if (state.allAgentsData) renderAllAgentsOverview(state.allAgentsData);
|
|
767
|
+
updateAllPricingDisplays();
|
|
761
768
|
showToast('✨ 已恢复所有系统默认预设模型');
|
|
762
769
|
}
|
|
763
770
|
|
|
@@ -1506,6 +1513,11 @@
|
|
|
1506
1513
|
el.subTotalTokens.textContent = `全平台 7 大 Agent 累计消耗`;
|
|
1507
1514
|
el.valTotalCost.textContent = formatKpiMainCost(model, grandCost);
|
|
1508
1515
|
el.valTotalCostUsd.textContent = formatKpiSubCost(model, grandCost);
|
|
1516
|
+
|
|
1517
|
+
const costBadge = document.querySelector('.highlight-card .kpi-badge');
|
|
1518
|
+
if (costBadge) costBadge.textContent = model.name;
|
|
1519
|
+
const costTitle = document.querySelector('.highlight-card .kpi-title');
|
|
1520
|
+
if (costTitle) costTitle.textContent = `${model.name} 等效费用`;
|
|
1509
1521
|
const hitRate = (gs.cacheHitRate !== undefined && gs.cacheHitRate !== null)
|
|
1510
1522
|
? gs.cacheHitRate
|
|
1511
1523
|
: (gs.inputTokens + gs.cacheTokens > 0 ? ((gs.cacheTokens / (gs.inputTokens + gs.cacheTokens)) * 100).toFixed(1) : '0.0');
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|