openclacky 1.2.7 → 1.2.8

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.
@@ -4,6 +4,7 @@
4
4
  const Billing = (() => {
5
5
  let _summary = null;
6
6
  let _daily = [];
7
+ let _sessions = []; // 会话列表
7
8
  let _allModels = []; // 保存完整的模型列表
8
9
  let _currentPeriod = "day";
9
10
  let _currentModel = "all";
@@ -73,15 +74,19 @@ const Billing = (() => {
73
74
 
74
75
  try {
75
76
  const modelParam = (_currentModel && _currentModel !== "all") ? `&model=${encodeURIComponent(_currentModel)}` : "";
76
- const [summaryRes, dailyRes] = await Promise.all([
77
+ const [summaryRes, dailyRes, sessionsRes] = await Promise.all([
77
78
  fetch(`/api/billing/summary?period=${_currentPeriod}${modelParam}`),
78
- fetch(`/api/billing/daily?days=30${modelParam}`)
79
+ fetch(`/api/billing/daily?days=30${modelParam}`),
80
+ fetch(`/api/billing/sessions?period=${_currentPeriod}${modelParam}&limit=100`)
79
81
  ]);
80
82
 
81
83
  _summary = await summaryRes.json();
82
84
  const dailyData = await dailyRes.json();
83
85
  _daily = dailyData.days || [];
84
86
 
87
+ const sessionsData = await sessionsRes.json();
88
+ _sessions = sessionsData.sessions || [];
89
+
85
90
  // 保存完整模型列表(仅在未筛选时更新)
86
91
  if (!_currentModel || _currentModel === "all") {
87
92
  _allModels = _summary.by_model ? Object.keys(_summary.by_model) : [];
@@ -172,6 +177,10 @@ const Billing = (() => {
172
177
  <div class="billing-chart-row">
173
178
  ${_renderCombinedChart()}
174
179
  </div>
180
+
181
+ <div class="billing-sessions-row">
182
+ ${_renderSessionList()}
183
+ </div>
175
184
  </div>
176
185
  `;
177
186
 
@@ -219,14 +228,19 @@ const Billing = (() => {
219
228
  <span class="tooltip-date">${date}</span>
220
229
  <span class="tooltip-total">${total} tokens</span>
221
230
  </div>
231
+ <div class="tooltip-row">
232
+ <span class="tooltip-dot tooltip-total"></span>
233
+ <span class="tooltip-label">${I18n.t("billing.totalTokens") || "Total Tokens"}</span>
234
+ <span class="tooltip-value">${total}</span>
235
+ </div>
222
236
  <div class="tooltip-row">
223
237
  <span class="tooltip-dot tooltip-cache-hit"></span>
224
- <span class="tooltip-label">${I18n.t("billing.inputCacheHit") || "Input (Cache Hit)"}</span>
238
+ <span class="tooltip-label">${I18n.t("billing.inputCacheHit") || "Input (Hit)"}</span>
225
239
  <span class="tooltip-value">${cacheHit}</span>
226
240
  </div>
227
241
  <div class="tooltip-row">
228
242
  <span class="tooltip-dot tooltip-cache-miss"></span>
229
- <span class="tooltip-label">${I18n.t("billing.inputCacheMiss") || "Input (Cache Miss)"}</span>
243
+ <span class="tooltip-label">${I18n.t("billing.inputCacheMiss") || "Input (Miss)"}</span>
230
244
  <span class="tooltip-value">${cacheMiss}</span>
231
245
  </div>
232
246
  <div class="tooltip-row">
@@ -333,35 +347,50 @@ const Billing = (() => {
333
347
  }
334
348
 
335
349
  function _renderTokenBreakdown() {
350
+ const totalTokens = _summary.total_tokens || 0;
351
+ const promptTokens = _summary.prompt_tokens || 0;
352
+ const completionTokens = _summary.completion_tokens || 0;
353
+ const cacheReadTokens = _summary.cache_read_tokens || 0;
354
+ const cacheMissTokens = promptTokens - cacheReadTokens;
355
+
336
356
  return `
337
357
  <div class="billing-section billing-token-section">
338
358
  <h3>${I18n.t("billing.tokenBreakdown") || "Token Breakdown"}</h3>
339
359
  <div class="billing-token-bars">
340
360
  <div class="billing-token-bar-item">
341
361
  <div class="billing-token-bar-header">
342
- <span class="billing-token-bar-label">${I18n.t("billing.inputCacheHit") || "Input (Cache Hit)"}</span>
343
- <span class="billing-token-bar-value">${_formatCompact(_summary.cache_read_tokens)}</span>
362
+ <span class="billing-token-bar-label">${I18n.t("billing.totalTokens") || "Total Tokens"}</span>
363
+ <span class="billing-token-bar-value">${_formatCompact(totalTokens)}</span>
344
364
  </div>
345
365
  <div class="billing-token-bar-track">
346
- <div class="billing-token-bar-fill billing-bar-cache-read" style="width: ${_getTokenPercent('cache_read')}%"></div>
366
+ <div class="billing-token-bar-fill billing-bar-total" style="width: 100%"></div>
347
367
  </div>
348
368
  </div>
349
369
  <div class="billing-token-bar-item">
350
370
  <div class="billing-token-bar-header">
351
- <span class="billing-token-bar-label">${I18n.t("billing.inputCacheMiss") || "Input (Cache Miss)"}</span>
352
- <span class="billing-token-bar-value">${_formatCompact(_summary.prompt_tokens)}</span>
371
+ <span class="billing-token-bar-label">${I18n.t("billing.inputCacheHit") || "Input (Hit)"}</span>
372
+ <span class="billing-token-bar-value">${_formatCompact(cacheReadTokens)}</span>
353
373
  </div>
354
374
  <div class="billing-token-bar-track">
355
- <div class="billing-token-bar-fill billing-bar-prompt" style="width: ${_getTokenPercent('prompt')}%"></div>
375
+ <div class="billing-token-bar-fill billing-bar-cache-read" style="width: ${_getTokenPercent(cacheReadTokens, totalTokens)}%"></div>
376
+ </div>
377
+ </div>
378
+ <div class="billing-token-bar-item">
379
+ <div class="billing-token-bar-header">
380
+ <span class="billing-token-bar-label">${I18n.t("billing.inputCacheMiss") || "Input (Miss)"}</span>
381
+ <span class="billing-token-bar-value">${_formatCompact(cacheMissTokens)}</span>
382
+ </div>
383
+ <div class="billing-token-bar-track">
384
+ <div class="billing-token-bar-fill billing-bar-cache-miss" style="width: ${_getTokenPercent(cacheMissTokens, totalTokens)}%"></div>
356
385
  </div>
357
386
  </div>
358
387
  <div class="billing-token-bar-item">
359
388
  <div class="billing-token-bar-header">
360
389
  <span class="billing-token-bar-label">${I18n.t("billing.output") || "Output"}</span>
361
- <span class="billing-token-bar-value">${_formatCompact(_summary.completion_tokens)}</span>
390
+ <span class="billing-token-bar-value">${_formatCompact(completionTokens)}</span>
362
391
  </div>
363
392
  <div class="billing-token-bar-track">
364
- <div class="billing-token-bar-fill billing-bar-completion" style="width: ${_getTokenPercent('completion')}%"></div>
393
+ <div class="billing-token-bar-fill billing-bar-completion" style="width: ${_getTokenPercent(completionTokens, totalTokens)}%"></div>
365
394
  </div>
366
395
  </div>
367
396
  </div>
@@ -369,9 +398,8 @@ const Billing = (() => {
369
398
  `;
370
399
  }
371
400
 
372
- function _getTokenPercent(type) {
373
- const total = _summary.total_tokens || 1;
374
- const value = _summary[type + '_tokens'] || 0;
401
+ function _getTokenPercent(value, total) {
402
+ if (!total || total === 0) return 0;
375
403
  return Math.min((value / total) * 100, 100).toFixed(1);
376
404
  }
377
405
 
@@ -427,9 +455,8 @@ const Billing = (() => {
427
455
 
428
456
  const recentDays = _daily.slice(-14);
429
457
  // Max values for scaling
430
- const maxInput = Math.max(...recentDays.map(d => (d.prompt_tokens || 0) + (d.cache_read_tokens || 0)), 1);
431
- const maxOutput = Math.max(...recentDays.map(d => d.completion_tokens || 0), 1);
432
- const maxVal = Math.max(maxInput, maxOutput);
458
+ const maxInput = Math.max(...recentDays.map(d => d.prompt_tokens || 0), 1);
459
+ const maxOutput = Math.max(...recentDays.map(d => d.completion_tokens || 0), 1); const maxVal = Math.max(maxInput, maxOutput);
433
460
 
434
461
  // Chart height in pixels
435
462
  const chartHeight = 120;
@@ -437,9 +464,10 @@ const Billing = (() => {
437
464
  // Generate bars: each date has Input (stacked: cache hit + cache miss) and Output
438
465
  const chartBars = recentDays.map((d, i) => {
439
466
  const cacheHit = d.cache_read_tokens || 0; // 命中缓存
440
- const cacheMiss = d.prompt_tokens || 0; // 未命中缓存(实际发送的prompt)
467
+ const totalPrompt = d.prompt_tokens || 0; // 全部输入token
468
+ const cacheMiss = totalPrompt - cacheHit; // 未命中缓存 = 全部输入 - 命中
441
469
  const output = d.completion_tokens || 0;
442
- const totalInput = cacheHit + cacheMiss;
470
+ const totalInput = totalPrompt;
443
471
  const totalTokens = totalInput + output;
444
472
 
445
473
  // Calculate heights in pixels
@@ -471,13 +499,17 @@ const Billing = (() => {
471
499
  <div class="billing-chart-header">
472
500
  <h4>${I18n.t("billing.dailyUsage") || "Usage Details"}</h4>
473
501
  <div class="billing-chart-legends">
502
+ <span class="billing-chart-legend">
503
+ <span class="billing-legend-dot billing-legend-total"></span>
504
+ ${I18n.t("billing.totalTokens") || "Total Tokens"}
505
+ </span>
474
506
  <span class="billing-chart-legend">
475
507
  <span class="billing-legend-dot billing-legend-cache-hit"></span>
476
- ${I18n.t("billing.inputCacheHit") || "Input (Cache Hit)"}
508
+ ${I18n.t("billing.inputCacheHit") || "Input (Hit)"}
477
509
  </span>
478
510
  <span class="billing-chart-legend">
479
511
  <span class="billing-legend-dot billing-legend-cache-miss"></span>
480
- ${I18n.t("billing.inputCacheMiss") || "Input (Cache Miss)"}
512
+ ${I18n.t("billing.inputCacheMiss") || "Input (Miss)"}
481
513
  </span>
482
514
  <span class="billing-chart-legend">
483
515
  <span class="billing-legend-dot billing-legend-output"></span>
@@ -493,6 +525,69 @@ const Billing = (() => {
493
525
  `;
494
526
  }
495
527
 
528
+ function _renderSessionList() {
529
+ if (!_sessions || _sessions.length === 0) {
530
+ return `
531
+ <div class="billing-section billing-sessions-section">
532
+ <h3>${I18n.t("billing.sessions") || "Sessions"}</h3>
533
+ <div class="billing-sessions-empty">${I18n.t("billing.noSessions") || "No session data"}</div>
534
+ </div>
535
+ `;
536
+ }
537
+
538
+ const rows = _sessions.map((s, index) => {
539
+ const sessionId = s.session_id || "unknown";
540
+ const isDeleted = s.is_deleted;
541
+ const sessionName = s.session_name || sessionId;
542
+ const displayName = isDeleted ? (I18n.t("billing.deletedSessions") || "已删除会话") : (sessionName.length > 25 ? sessionName.slice(0, 25) + "..." : sessionName);
543
+ const totalCost = _convertCost(s.total_cost || 0);
544
+ const totalTokens = s.total_tokens || 0;
545
+ const promptTokens = s.prompt_tokens || 0;
546
+ const cacheHit = s.cache_read_tokens || 0;
547
+ const cacheMiss = promptTokens - cacheHit;
548
+ const completionTokens = s.completion_tokens || 0;
549
+ const requests = s.requests || 0;
550
+ const models = (s.models || []).join(", ");
551
+ const lastRequest = s.last_request ? new Date(s.last_request).toLocaleString() : "-";
552
+ const rowClass = isDeleted ? "billing-session-row billing-session-deleted" : "billing-session-row";
553
+
554
+ return `
555
+ <div class="${rowClass}" data-session-id="${_esc(sessionId)}">
556
+ <div class="billing-cell billing-cell-index">${index + 1}</div>
557
+ <div class="billing-cell billing-cell-session" title="${_esc(sessionName)}">
558
+ <span class="billing-cell-main">${_esc(displayName)}</span>
559
+ <span class="billing-cell-sub">${requests} ${I18n.t("billing.requests") || "req"} · ${_esc(models)}</span>
560
+ </div>
561
+ <div class="billing-cell billing-cell-number">${_formatCompact(totalTokens)}</div>
562
+ <div class="billing-cell billing-cell-number billing-cell-hit">${_formatCompact(cacheHit)}</div>
563
+ <div class="billing-cell billing-cell-number billing-cell-miss">${_formatCompact(cacheMiss)}</div>
564
+ <div class="billing-cell billing-cell-number">${_formatCompact(completionTokens)}</div>
565
+ <div class="billing-cell billing-cell-cost">${_getCurrencySymbol()}${_formatCost(totalCost)}</div>
566
+ <div class="billing-cell billing-cell-time">${lastRequest}</div>
567
+ </div>
568
+ `;
569
+ }).join("");
570
+
571
+ return `
572
+ <div class="billing-section billing-sessions-section">
573
+ <h3>${I18n.t("billing.sessions") || "Sessions"}</h3>
574
+ <div class="billing-sessions-header">
575
+ <span class="billing-cell billing-cell-index">#</span>
576
+ <span class="billing-cell billing-cell-session">${I18n.t("billing.sessionId") || "Session"}</span>
577
+ <span class="billing-cell billing-cell-number">${I18n.t("billing.headerTotal") || "总消耗"}</span>
578
+ <span class="billing-cell billing-cell-number">${I18n.t("billing.headerHit") || "命中"}</span>
579
+ <span class="billing-cell billing-cell-number">${I18n.t("billing.headerMiss") || "未命中"}</span>
580
+ <span class="billing-cell billing-cell-number">${I18n.t("billing.headerOutput") || "输出"}</span>
581
+ <span class="billing-cell billing-cell-cost">${I18n.t("billing.cost") || "Cost"}</span>
582
+ <span class="billing-cell billing-cell-time">${I18n.t("billing.lastRequest") || "Time"}</span>
583
+ </div>
584
+ <div class="billing-sessions-list">
585
+ ${rows}
586
+ </div>
587
+ </div>
588
+ `;
589
+ }
590
+
496
591
  // ── Helpers ─────────────────────────────────────────────────────────────────
497
592
 
498
593
  function _formatCost(cost) {
@@ -686,15 +686,25 @@ const I18n = (() => {
686
686
  "billing.cacheHit": "Cache Hit",
687
687
  "billing.inputCacheHit": "Input (Cache Hit)",
688
688
  "billing.inputCacheMiss": "Input (Cache Miss)",
689
- "billing.output": "Output",
690
- "billing.tokenUsage": "Token Usage",
689
+ "billing.totalInput": "Total Input",
690
+ "billing.output": "Output", "billing.tokenUsage": "Token Usage",
691
691
  "billing.costTrend": "Cost Trend",
692
692
  "billing.noData": "No data available",
693
693
 
694
694
  "error.insufficient_credit": "Insufficient LLM credit. Please top up your account to continue.",
695
695
  "error.insufficient_credit.action": "Top up",
696
- },
697
696
 
697
+ "billing.sessions": "Sessions",
698
+ "billing.sessionId": "Session",
699
+ "billing.tokens": "Tokens",
700
+ "billing.lastRequest": "Last Request",
701
+ "billing.noSessions": "No session data",
702
+ "billing.deletedSessions": "Deleted Sessions",
703
+ "billing.headerTotal": "Total",
704
+ "billing.headerHit": "Hit",
705
+ "billing.headerMiss": "Miss",
706
+ "billing.headerOutput": "Output",
707
+ },
698
708
  zh: {
699
709
  // ── Sidebar ──
700
710
  "sidebar.chat": "会话",
@@ -1366,16 +1376,26 @@ const I18n = (() => {
1366
1376
  "billing.cacheHit": "缓存命中",
1367
1377
  "billing.inputCacheHit": "输入(命中缓存)",
1368
1378
  "billing.inputCacheMiss": "输入(未命中缓存)",
1369
- "billing.output": "输出",
1370
- "billing.tokenUsage": "Token 用量",
1379
+ "billing.totalTokens": "Token 总消耗",
1380
+ "billing.totalInput": "总输入",
1381
+ "billing.output": "输出", "billing.tokenUsage": "Token 用量",
1371
1382
  "billing.costTrend": "费用趋势",
1372
1383
  "billing.noData": "暂无数据",
1373
1384
 
1374
1385
  "error.insufficient_credit": "LLM(大模型)余额不足,请充值后继续使用。",
1375
1386
  "error.insufficient_credit.action": "去充值",
1387
+ "billing.sessions": "会话消耗",
1388
+ "billing.sessionId": "会话",
1389
+ "billing.tokens": "Token",
1390
+ "billing.lastRequest": "最后请求",
1391
+ "billing.noSessions": "暂无会话数据",
1392
+ "billing.deletedSessions": "已删除会话",
1393
+ "billing.headerTotal": "总消耗",
1394
+ "billing.headerHit": "命中",
1395
+ "billing.headerMiss": "未命中",
1396
+ "billing.headerOutput": "输出",
1376
1397
  }
1377
1398
  };
1378
-
1379
1399
  // ── State ──────────────────────────────────────────────────────────────────
1380
1400
  let _lang = localStorage.getItem(STORAGE_KEY) || DEFAULT_LANG;
1381
1401
 
data/lib/clacky.rb CHANGED
@@ -83,6 +83,7 @@ require_relative "clacky/idle_compression_timer"
83
83
  # Agent modules
84
84
  require_relative "clacky/agent/message_compressor"
85
85
  require_relative "clacky/agent/hook_manager"
86
+ require_relative "clacky/shell_hook_loader"
86
87
  require_relative "clacky/agent/tool_registry"
87
88
 
88
89
  # UI modules
@@ -132,6 +133,11 @@ require_relative "clacky/server/web_ui_controller"
132
133
  require_relative "clacky/server/browser_manager"
133
134
  require_relative "clacky/cli"
134
135
 
136
+ # Runtime patch layer: load user/AI patches from ~/.clacky/patches/ after all
137
+ # gem code is defined, so fingerprints reflect the actual installed source.
138
+ require_relative "clacky/patch_loader"
139
+ Clacky::PatchLoader.load_all
140
+
135
141
  module Clacky
136
142
  class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
137
143
  class AgentError < StandardError; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.7
4
+ version: 1.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-31 00:00:00.000000000 Z
11
+ date: 2026-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -374,6 +374,7 @@ files:
374
374
  - lib/clacky/default_skills/cron-task-creator/SKILL.md
375
375
  - lib/clacky/default_skills/cron-task-creator/evals/evals.json
376
376
  - lib/clacky/default_skills/deploy/SKILL.md
377
+ - lib/clacky/default_skills/extend-openclacky/SKILL.md
377
378
  - lib/clacky/default_skills/mcp-manager/SKILL.md
378
379
  - lib/clacky/default_skills/new/SKILL.md
379
380
  - lib/clacky/default_skills/new/scripts/create_rails_project.sh
@@ -417,6 +418,7 @@ files:
417
418
  - lib/clacky/message_format/open_ai.rb
418
419
  - lib/clacky/message_history.rb
419
420
  - lib/clacky/openai_stream_aggregator.rb
421
+ - lib/clacky/patch_loader.rb
420
422
  - lib/clacky/plain_ui_controller.rb
421
423
  - lib/clacky/platform_http_client.rb
422
424
  - lib/clacky/providers.rb
@@ -444,6 +446,7 @@ files:
444
446
  - lib/clacky/server/channel/channel_config.rb
445
447
  - lib/clacky/server/channel/channel_manager.rb
446
448
  - lib/clacky/server/channel/channel_ui_controller.rb
449
+ - lib/clacky/server/channel/user_adapter_loader.rb
447
450
  - lib/clacky/server/discover.rb
448
451
  - lib/clacky/server/epipe_safe_io.rb
449
452
  - lib/clacky/server/http_server.rb
@@ -452,6 +455,7 @@ files:
452
455
  - lib/clacky/server/session_registry.rb
453
456
  - lib/clacky/server/web_ui_controller.rb
454
457
  - lib/clacky/session_manager.rb
458
+ - lib/clacky/shell_hook_loader.rb
455
459
  - lib/clacky/skill.rb
456
460
  - lib/clacky/skill_loader.rb
457
461
  - lib/clacky/telemetry.rb