Skip to content

Commit eaecb8c

Browse files
zz314657917李杰
authored andcommitted
feat(codex): 为会话管理器添加 Token 使用统计
- 更新 CodexSessionRecord 以包含 inputTokens、outputTokens 和 totalTokens。 - 实现 formatLargeNumber 和 formatTokenStats 函数来显示 Token 使用情况。 - 增强 CodexSessionManager 以在 UI 中显示 Token 使用情况。 - 添加 Token 使用统计对应的英文和中文翻译。 - 更新 Token 使用显示的样式。
1 parent e07cb76 commit eaecb8c

7 files changed

Lines changed: 110 additions & 6 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/src/modules/codex_session_manager.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ pub struct CodexSessionRecord {
3434
pub updated_at: Option<i64>,
3535
pub location_count: usize,
3636
pub locations: Vec<CodexSessionLocation>,
37+
/// 输入token数量(来自rollout文件的token_count记录)
38+
#[serde(skip_serializing_if = "Option::is_none")]
39+
pub input_tokens: Option<u64>,
40+
/// 输出token数量(来自rollout文件的token_count记录)
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub output_tokens: Option<u64>,
43+
/// 总token数量(来自rollout文件的token_count记录)
44+
#[serde(skip_serializing_if = "Option::is_none")]
45+
pub total_tokens: Option<u64>,
3746
}
3847

3948
#[derive(Debug, Clone, Serialize)]
@@ -148,6 +157,54 @@ struct TrashedSessionEntry {
148157
trashed_rollout_path: PathBuf,
149158
}
150159

160+
/// 从 rollout JSONL 文件中读取 token 统计信息
161+
/// 返回 (input_tokens, output_tokens, total_tokens)
162+
fn read_token_stats_from_rollout(rollout_path: &Path) -> Option<(u64, u64, u64)> {
163+
let content = fs::read_to_string(rollout_path).ok()?;
164+
165+
// 从文件末尾向前读取,找到第一条 token_count 记录
166+
// token_count 记录通常在文件末尾
167+
for line in content.lines().rev() {
168+
let trimmed = line.trim();
169+
if trimmed.is_empty() {
170+
continue;
171+
}
172+
173+
if let Ok(parsed) = serde_json::from_str::<JsonValue>(trimmed) {
174+
// 检查 type == "event_msg"
175+
if parsed.get("type").and_then(|v| v.as_str()) != Some("event_msg") {
176+
continue;
177+
}
178+
179+
if let Some(payload) = parsed.get("payload") {
180+
// 检查 payload.type == "token_count"
181+
if payload.get("type").and_then(|v| v.as_str()) != Some("token_count") {
182+
continue;
183+
}
184+
185+
if let Some(info) = payload.get("info") {
186+
if let Some(usage) = info.get("total_token_usage") {
187+
let input = usage
188+
.get("input_tokens")
189+
.and_then(|v| v.as_u64())
190+
.unwrap_or(0);
191+
let output = usage
192+
.get("output_tokens")
193+
.and_then(|v| v.as_u64())
194+
.unwrap_or(0);
195+
let total = usage
196+
.get("total_tokens")
197+
.and_then(|v| v.as_u64())
198+
.unwrap_or(0);
199+
return Some((input, output, total));
200+
}
201+
}
202+
}
203+
}
204+
}
205+
None
206+
}
207+
151208
pub fn list_sessions_across_instances() -> Result<Vec<CodexSessionRecord>, String> {
152209
let instances = collect_instances()?;
153210
let process_entries = modules::process::collect_codex_process_entries();
@@ -156,6 +213,9 @@ pub fn list_sessions_across_instances() -> Result<Vec<CodexSessionRecord>, Strin
156213
for instance in &instances {
157214
let running = is_instance_running(instance, &process_entries);
158215
for snapshot in load_thread_snapshots(instance)? {
216+
// 读取 token 统计
217+
let token_stats = read_token_stats_from_rollout(&snapshot.rollout_path);
218+
159219
let entry =
160220
session_map
161221
.entry(snapshot.id.clone())
@@ -166,6 +226,9 @@ pub fn list_sessions_across_instances() -> Result<Vec<CodexSessionRecord>, Strin
166226
updated_at: snapshot.updated_at,
167227
location_count: 0,
168228
locations: Vec::new(),
229+
input_tokens: token_stats.as_ref().map(|(i, _, _)| *i),
230+
output_tokens: token_stats.as_ref().map(|(_, o, _)| *o),
231+
total_tokens: token_stats.as_ref().map(|(_, _, t)| *t),
169232
});
170233

171234
if entry.updated_at.is_none() {

src/components/codex/CodexSessionManager.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,26 @@ function formatSessionId(sessionId: string): string {
7575
return `${sessionId.slice(0, 8)}...${sessionId.slice(-6)}`;
7676
}
7777

78+
function formatLargeNumber(value: number): string {
79+
if (value >= 1_000_000) {
80+
return `${(value / 1_000_000).toFixed(1)}M`;
81+
}
82+
if (value >= 1_000) {
83+
return `${(value / 1_000).toFixed(1)}K`;
84+
}
85+
return value.toLocaleString();
86+
}
87+
88+
function formatTokenStats(session: CodexSessionRecord): string {
89+
const { inputTokens, outputTokens } = session;
90+
91+
if (inputTokens !== undefined && outputTokens !== undefined) {
92+
return `${formatLargeNumber(inputTokens)} / ${formatLargeNumber(outputTokens)} tokens`;
93+
}
94+
95+
return '';
96+
}
97+
7898
export function CodexSessionManager() {
7999
const { t, i18n } = useTranslation();
80100
const instances = useCodexInstanceStore((state) => state.instances);
@@ -547,6 +567,11 @@ export function CodexSessionManager() {
547567
>
548568
{copiedSessionId === session.sessionId ? <Check size={14} /> : <Copy size={14} />}
549569
</button>
570+
{formatTokenStats(session) && (
571+
<span className="codex-session-row__tokens" title={t('codex.sessionManager.labels.tokenUsage', 'Token使用')}>
572+
{formatTokenStats(session)}
573+
</span>
574+
)}
550575
<span className="codex-session-row__time">
551576
{formatRelativeTime(session.updatedAt, isZh)}
552577
</span>

src/locales/en.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,11 +2191,12 @@
21912191
"groupCount": "{{count}} sessions",
21922192
"updatedAt": "Updated",
21932193
"labels": {
2194-
"sessionId": "Session ID"
2194+
"sessionId": "Session ID",
2195+
"tokenUsage": "Token Usage"
21952196
},
21962197
"locationRunning": " (running)",
21972198
"untitled": "Untitled session",
2198-
"restoreModal": {
2199+
"restoreModal": {
21992200
"title": "Restore Sessions",
22002201
"hint": "Restoring puts the rollout file, SQLite thread row, and session_index entry back into the original instance together.",
22012202
"emptyTitle": "Trash is empty",

src/locales/zh-CN.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,11 +2191,12 @@
21912191
"groupCount": "{{count}} 条会话",
21922192
"updatedAt": "最近更新",
21932193
"labels": {
2194-
"sessionId": "会话 ID"
2194+
"sessionId": "会话 ID",
2195+
"tokenUsage": "Token使用"
21952196
},
21962197
"locationRunning": "(运行中)",
21972198
"untitled": "未命名会话",
2198-
"restoreModal": {
2199+
"restoreModal": {
21992200
"title": "恢复会话",
22002201
"hint": "恢复会把 rollout 文件、SQLite 线程记录和 session_index 条目一起放回原实例。",
22012202
"emptyTitle": "废纸篓里还没有会话",

src/styles/pages/codex.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5421,6 +5421,14 @@
54215421
outline-offset: 2px;
54225422
}
54235423

5424+
.codex-session-row__tokens {
5425+
font-size: 11px;
5426+
color: var(--text-muted);
5427+
margin-right: 8px;
5428+
white-space: nowrap;
5429+
opacity: 0.85;
5430+
}
5431+
54245432
.codex-session-restore-modal {
54255433
width: min(760px, calc(100vw - 32px));
54265434
}

src/types/codex.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ export interface CodexSessionRecord {
137137
updatedAt?: number | null;
138138
locationCount: number;
139139
locations: CodexSessionLocation[];
140+
/** 输入token数量(来自rollout文件的token_count记录) */
141+
inputTokens?: number;
142+
/** 输出token数量(来自rollout文件的token_count记录) */
143+
outputTokens?: number;
144+
/** 总token数量(来自rollout文件的token_count记录) */
145+
totalTokens?: number;
140146
}
141147

142148
export interface CodexSessionTrashSummary {

0 commit comments

Comments
 (0)