Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Run PowerShell tests
- name: Run PowerShell 7 tests
shell: pwsh
run: .\tests\test-refresh.ps1

- name: Run Windows PowerShell 5.1 tests
shell: powershell
run: .\tests\test-refresh.ps1
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
- 展示缓存命中率、月度环比、日均用量与单次响应均值。
- 支持年度相对色阶与固定色阶,兼顾个人高峰和跨月比较。
- 提供当前指标的月度趋势图,并区分今天、未来日期和无使用日期。
- 刷新浏览器或点击页面按钮时自动重新读取本地统计。
- 汇总模型响应次数与本地会话文件数量。
- 提供官方 API 单价参考和基础费用估算。
- 无需安装依赖或启动 Web 服务,双击即可运行
- 无需安装第三方依赖,双击即可启动本地看板服务
- 只在本机处理数据,不上传会话记录或统计结果。

## 系统要求
Expand All @@ -32,14 +33,17 @@

## 使用方法

下载或克隆本仓库,然后双击项目根目录中的 `start-dashboard.cmd`。
下载或克隆本仓库,然后双击项目根目录中的 `start-dashboard.cmd`。启动后请保留命令窗口;关闭窗口或按 `Ctrl+C` 会停止本地服务。

启动程序会:

1. 从 `$env:CODEX_HOME` 读取 Codex 数据;未设置时使用 `~/.codex`。
2. 扫描 `sessions` 和 `archived_sessions` 下的 JSONL 会话记录。
3. 只提取日期和 Token 统计,不读取或保存提示词、回答正文、凭据或项目内容。
4. 更新本地数据文件,并用默认浏览器打开日历。
4. 启动只监听 `127.0.0.1:8765` 的本地服务,并用默认浏览器打开日历。
5. 每次刷新浏览器或点击“刷新数据”时,重新生成并展示最新统计。

本地服务只允许本机访问,不会把会话记录或汇总数据发送到外部网络。直接双击 `index.html` 仍可查看上一次生成的数据,但页面内刷新功能不可用。

也可以只刷新数据:

Expand Down
73 changes: 61 additions & 12 deletions assets/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(() => {
'use strict';

const data = window.CODEX_TOKEN_CALENDAR_DATA || null;
let data = window.CODEX_TOKEN_CALENDAR_DATA || null;
const pricing = window.CODEX_PRICING_REFERENCE || null;
const metrics = {
total: { label: '总 Token', description: '输入 Token 与输出 Token 之和' },
Expand All @@ -15,6 +15,7 @@

const elements = {
status: document.getElementById('data-status'),
refresh: document.getElementById('refresh-data'),
metric: document.getElementById('metric-select'),
scale: document.getElementById('scale-select'),
picker: document.getElementById('month-picker'),
Expand Down Expand Up @@ -55,7 +56,7 @@
const detailDate = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: 'long', day: 'numeric', weekday: 'short'
});
const byDate = new Map((data?.days || []).map(day => [day.date, day]));
let byDate = new Map((data?.days || []).map(day => [day.date, day]));
const now = new Date();
const todayKey = isoDate(now.getFullYear(), now.getMonth(), now.getDate());
let viewDate = new Date(now.getFullYear(), now.getMonth(), 1);
Expand All @@ -73,6 +74,62 @@
return Number(day?.[metric] || 0);
}

function renderDataStatus(prefix) {
if (!data) {
elements.status.textContent = prefix || '尚未生成数据,请运行 start-dashboard.cmd。';
return;
}

const generated = new Date(data.generatedAt);
const summary = data.sourceSummary;
elements.status.textContent =
`${prefix ? `${prefix} · ` : ''}更新于 ${generated.toLocaleString('zh-CN')} · ` +
`${summary.sessionFiles} 个会话文件 · ${summary.tokenRecords} 条 Token 记录` +
(summary.parseErrors ? ` · 跳过 ${summary.parseErrors} 条异常记录` : '');
}

async function refreshData() {
if (location.protocol === 'file:') {
elements.status.textContent = '页面内刷新需要通过 start-dashboard.cmd 启动本地服务。';
elements.refresh.className = 'refresh-button is-error';
return;
}

elements.refresh.disabled = true;
elements.refresh.className = 'refresh-button is-loading';
elements.refresh.textContent = '正在刷新…';
elements.status.textContent = '正在重新读取本地 Codex Token 统计…';

try {
const response = await fetch('/api/refresh', {
method: 'POST',
cache: 'no-store',
headers: { Accept: 'application/json' }
});
const payload = await response.json();
if (!response.ok || !payload.ok || !payload.data) {
throw new Error(payload.error || `服务返回 ${response.status}`);
}

data = payload.data;
window.CODEX_TOKEN_CALENDAR_DATA = data;
byDate = new Map((data.days || []).map(day => [day.date, day]));
selectedDate = null;
renderDataStatus('刷新成功');
renderCalendar();
elements.refresh.className = 'refresh-button is-success';
elements.refresh.textContent = '刷新完成';
}
catch (error) {
renderDataStatus(`刷新失败:${error.message}`);
elements.refresh.className = 'refresh-button is-error';
elements.refresh.textContent = '重试刷新';
}
finally {
elements.refresh.disabled = false;
}
}

function formatValue(value, metric, compact = true) {
if (metrics[metric].percentage) return `${value.toFixed(1)}%`;
return (compact ? compactNumber : fullNumber).format(value);
Expand Down Expand Up @@ -398,17 +455,9 @@
`价格核对日期:${pricing.verifiedAt}。${pricing.notes.join(' ')}`;
}

if (data) {
const generated = new Date(data.generatedAt);
const summary = data.sourceSummary;
elements.status.textContent =
`更新于 ${generated.toLocaleString('zh-CN')} · ${summary.sessionFiles} 个会话文件 · ` +
`${summary.tokenRecords} 条 Token 记录` +
(summary.parseErrors ? ` · 跳过 ${summary.parseErrors} 条异常记录` : '');
} else {
elements.status.textContent = '尚未生成数据,请运行 start-dashboard.cmd。';
}
renderDataStatus();

elements.refresh.addEventListener('click', refreshData);
elements.metric.addEventListener('change', renderCalendar);
elements.scale.addEventListener('change', renderCalendar);
elements.previous.addEventListener('click', () => shiftMonth(-1));
Expand Down
7 changes: 6 additions & 1 deletion assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ h2 { margin-bottom: 0; font-size: clamp(21px, 3vw, 30px); }
.toolbar select, .toolbar input[type="month"] { min-height: 42px; padding: 0 12px; }
.toolbar select { min-width: 190px; }
.month-controls { display: flex; flex-wrap: wrap; gap: 8px; }
.month-controls button:first-child, .month-controls button:last-child { font-size: 24px; }
.month-controls button[aria-label="上一个月"],
.month-controls button[aria-label="下一个月"] { font-size: 24px; }
.refresh-button { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.refresh-button.is-loading { cursor: wait; opacity: 0.72; }
.refresh-button.is-success { border-color: var(--level-1); color: var(--level-1); }
.refresh-button.is-error { border-color: var(--level-4); color: var(--level-4); }

abbr[title] {
display: inline-grid;
Expand Down
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div>
<p class="eyebrow">LOCAL CODEX USAGE</p>
<h1>Codex Token 日历</h1>
<p id="data-status" class="muted">正在读取本地统计数据…</p>
<p id="data-status" class="muted" aria-live="polite">正在读取本地统计数据…</p>
</div>
<div class="header-accent" aria-hidden="true"></div>
</header>
Expand All @@ -40,6 +40,7 @@ <h1>Codex Token 日历</h1>
</select>
</label>
<div class="month-controls">
<button id="refresh-data" class="refresh-button" type="button">刷新数据</button>
<button id="previous-month" type="button" aria-label="上一个月">‹</button>
<input id="month-picker" type="month" aria-label="选择月份">
<button id="current-month" type="button">本月</button>
Expand Down
13 changes: 12 additions & 1 deletion scripts/refresh-data.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,18 @@ foreach ($filePath in @($filesByName.Values | Sort-Object)) {
}

try {
$timestamp = [DateTimeOffset]::Parse([string]$record.timestamp)
# PowerShell 7 会把 ISO 时间戳反序列化为 DateTime;先转成字符串会丢失
# Utc Kind,导致跨日记录不再按北京时间偏移。Windows PowerShell 5.1
# 保留字符串,因此需要同时兼容两种运行时返回类型。
if ($record.timestamp -is [DateTime]) {
$timestamp = [DateTimeOffset]$record.timestamp
}
elseif ($record.timestamp -is [DateTimeOffset]) {
$timestamp = $record.timestamp
}
else {
$timestamp = [DateTimeOffset]::Parse([string]$record.timestamp)
}
$localTime = [TimeZoneInfo]::ConvertTime($timestamp, $chinaTimeZone)
$dateKey = $localTime.ToString('yyyy-MM-dd')
$usage = $record.payload.info.last_token_usage
Expand Down
Loading
Loading