codexmate 0.0.36 → 0.0.38

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.
package/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  [![Version](https://img.shields.io/npm/v/codexmate?style=flat-square&color=A179FF)](https://www.npmjs.com/package/codexmate)
16
16
  [![Build](https://img.shields.io/github/actions/workflow/status/SakuraByteCore/codexmate/release.yml?style=flat-square&color=44cc11)](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
17
17
  [![Downloads](https://img.shields.io/npm/dt/codexmate?style=flat-square)](https://www.npmjs.com/package/codexmate)
18
+ [![Install](https://img.shields.io/badge/install-brew%20%7C%20curl%20%7C%20npm-0A0?style=flat-square)](#install-via-homebrew-macos--linux)
18
19
  [![Platform](https://img.shields.io/badge/platform-Termux%20%7C%20Linux%20%7C%20macOS%20%7C%20Windows-555?style=flat-square)](#quick-start)
19
20
  [![Node](https://img.shields.io/node/v/codexmate?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/)
20
21
  [![License](https://img.shields.io/npm/l/codexmate?style=flat-square)](LICENSE)
@@ -70,11 +71,19 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
70
71
 
71
72
  ## Quick Start
72
73
 
74
+ ### Install via Homebrew (macOS / Linux)
75
+
76
+ ```bash
77
+ brew tap SakuraByteCore/codexmate
78
+ brew install codexmate
79
+ ```
80
+
81
+ Requires [Node.js](https://nodejs.org/) (`brew install node` if not present).
82
+
73
83
  ### Install via npm
74
84
 
75
85
  ```bash
76
86
  npm install -g codexmate
77
- codexmate setup
78
87
  codexmate run
79
88
  ```
80
89
 
@@ -102,7 +111,7 @@ flowchart TD
102
111
  CLI[CLI]
103
112
  WebUI[Web UI]
104
113
  MCP[MCP Server]
105
-
114
+
106
115
  subgraph Mate [Codex Mate Core]
107
116
  API[HTTP API]
108
117
  Config[Config Engine]
@@ -110,7 +119,7 @@ flowchart TD
110
119
  Skills[Skills Market]
111
120
  Tasks[Task Runner]
112
121
  end
113
-
122
+
114
123
  subgraph Local [Local Filesystem]
115
124
  CodexDir[~/.codex]
116
125
  ClaudeDir[~/.claude]
@@ -120,9 +129,9 @@ flowchart TD
120
129
 
121
130
  User --> CLI & WebUI & MCP
122
131
  CLI & WebUI & MCP --> API
123
-
132
+
124
133
  API --> Config & Session & Skills & Tasks
125
-
134
+
126
135
  Config --> CodexDir & ClaudeDir & ClawDir
127
136
  Session --> State
128
137
  Skills --> Local
package/README.zh.md CHANGED
@@ -15,6 +15,7 @@
15
15
  [![Version](https://img.shields.io/npm/v/codexmate?style=flat-square&color=A179FF)](https://www.npmjs.com/package/codexmate)
16
16
  [![Build](https://img.shields.io/github/actions/workflow/status/SakuraByteCore/codexmate/release.yml?style=flat-square&color=44cc11)](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
17
17
  [![Downloads](https://img.shields.io/npm/dt/codexmate?style=flat-square)](https://www.npmjs.com/package/codexmate)
18
+ [![Install](https://img.shields.io/badge/install-brew%20%7C%20curl%20%7C%20npm-0A0?style=flat-square)](#homebrew-安装macos--linux)
18
19
  [![Platform](https://img.shields.io/badge/platform-Termux%20%7C%20Linux%20%7C%20macOS%20%7C%20Windows-555?style=flat-square)](#快速开始)
19
20
  [![Node](https://img.shields.io/node/v/codexmate?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/)
20
21
  [![License](https://img.shields.io/npm/l/codexmate?style=flat-square)](LICENSE)
@@ -70,11 +71,19 @@
70
71
 
71
72
  ## 快速开始
72
73
 
74
+ ### Homebrew 安装(macOS / Linux)
75
+
76
+ ```bash
77
+ brew tap SakuraByteCore/codexmate
78
+ brew install codexmate
79
+ ```
80
+
81
+ 需要 [Node.js](https://nodejs.org/)(如未安装可执行 `brew install node`)。
82
+
73
83
  ### 通过 npm 安装
74
84
 
75
85
  ```bash
76
86
  npm install -g codexmate
77
- codexmate setup
78
87
  codexmate run
79
88
  ```
80
89
 
@@ -102,7 +111,7 @@ flowchart TD
102
111
  CLI[CLI 命令]
103
112
  WebUI[Web 界面]
104
113
  MCP[MCP 服务]
105
-
114
+
106
115
  subgraph Mate [Codex Mate 核心]
107
116
  API[HTTP API]
108
117
  Config[配置引擎]
@@ -110,7 +119,7 @@ flowchart TD
110
119
  Skills[Skills 市场]
111
120
  Tasks[任务运行器]
112
121
  end
113
-
122
+
114
123
  subgraph Local [本地文件系统]
115
124
  CodexDir[~/.codex]
116
125
  ClaudeDir[~/.claude]
@@ -120,9 +129,9 @@ flowchart TD
120
129
 
121
130
  User --> CLI & WebUI & MCP
122
131
  CLI & WebUI & MCP --> API
123
-
132
+
124
133
  API --> Config & Session & Skills & Tasks
125
-
134
+
126
135
  Config --> CodexDir & ClaudeDir & ClawDir
127
136
  Session --> State
128
137
  Skills --> Local
@@ -0,0 +1,68 @@
1
+ function parseAnalyticsExportArgs(args = []) {
2
+ const options = {
3
+ format: 'csv',
4
+ source: 'all',
5
+ output: ''
6
+ };
7
+ const errors = [];
8
+ for (let index = 0; index < args.length; index += 1) {
9
+ const token = String(args[index] || '');
10
+ const readValue = (flag) => {
11
+ if (token.startsWith(`${flag}=`)) {
12
+ return token.slice(flag.length + 1);
13
+ }
14
+ const value = args[index + 1];
15
+ index += 1;
16
+ return value;
17
+ };
18
+ if (token === '--format' || token.startsWith('--format=')) {
19
+ options.format = String(readValue('--format') || '').trim().toLowerCase();
20
+ continue;
21
+ }
22
+ if (token === '--from' || token.startsWith('--from=')) {
23
+ options.from = String(readValue('--from') || '').trim();
24
+ continue;
25
+ }
26
+ if (token === '--to' || token.startsWith('--to=')) {
27
+ options.to = String(readValue('--to') || '').trim();
28
+ continue;
29
+ }
30
+ if (token === '--model' || token.startsWith('--model=')) {
31
+ options.model = String(readValue('--model') || '').trim();
32
+ continue;
33
+ }
34
+ if (token === '--source' || token.startsWith('--source=')) {
35
+ options.source = String(readValue('--source') || '').trim().toLowerCase();
36
+ continue;
37
+ }
38
+ if (token === '--output' || token === '-o' || token.startsWith('--output=')) {
39
+ options.output = String(readValue(token === '-o' ? '-o' : '--output') || '').trim();
40
+ continue;
41
+ }
42
+ if (token === '--force-refresh') {
43
+ options.forceRefresh = true;
44
+ continue;
45
+ }
46
+ if (token === '--help' || token === '-h') {
47
+ options.help = true;
48
+ continue;
49
+ }
50
+ if (token) {
51
+ errors.push(`未知参数 ${token}`);
52
+ }
53
+ }
54
+ if (options.format !== 'csv' && options.format !== 'json') {
55
+ errors.push('--format 必须是 csv 或 json');
56
+ }
57
+ if (options.source && !['codex', 'claude', 'gemini', 'codebuddy', 'all'].includes(options.source)) {
58
+ errors.push('--source 必须是 codex、claude、gemini、codebuddy 或 all');
59
+ }
60
+ return {
61
+ options,
62
+ error: errors.join(';')
63
+ };
64
+ }
65
+
66
+ module.exports = {
67
+ parseAnalyticsExportArgs
68
+ };
@@ -113,6 +113,192 @@ async function listSessionUsageCore(params = {}, deps = {}) {
113
113
  return normalizedSessions.filter(Boolean);
114
114
  }
115
115
 
116
+ function readNonNegativeInteger(value) {
117
+ const numeric = Number(value);
118
+ if (!Number.isFinite(numeric) || numeric < 0) {
119
+ return 0;
120
+ }
121
+ return Math.floor(numeric);
122
+ }
123
+
124
+ function parseUsageExportDate(value, boundary) {
125
+ if (value === undefined || value === null || value === '') {
126
+ return null;
127
+ }
128
+ if (value instanceof Date) {
129
+ const time = value.getTime();
130
+ return Number.isFinite(time) ? time : NaN;
131
+ }
132
+ const raw = String(value).trim();
133
+ if (!raw) {
134
+ return null;
135
+ }
136
+ const dateOnly = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
137
+ if (dateOnly) {
138
+ const year = Number(dateOnly[1]);
139
+ const month = Number(dateOnly[2]) - 1;
140
+ const day = Number(dateOnly[3]);
141
+ const start = Date.UTC(year, month, day);
142
+ const normalized = new Date(start);
143
+ if (!Number.isFinite(start)
144
+ || normalized.getUTCFullYear() !== year
145
+ || normalized.getUTCMonth() !== month
146
+ || normalized.getUTCDate() !== day) {
147
+ return NaN;
148
+ }
149
+ return boundary === 'end' ? start + 24 * 60 * 60 * 1000 : start;
150
+ }
151
+ const parsed = Date.parse(raw);
152
+ return Number.isFinite(parsed) ? parsed : NaN;
153
+ }
154
+
155
+ function formatUsageExportDay(timestamp) {
156
+ return new Date(timestamp).toISOString().slice(0, 10);
157
+ }
158
+
159
+ function normalizeUsageExportFormat(value) {
160
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
161
+ return normalized === 'json' ? 'json' : 'csv';
162
+ }
163
+
164
+ function normalizeUsageExportModelFilters(params = {}) {
165
+ const raw = [];
166
+ const push = (value) => {
167
+ if (Array.isArray(value)) {
168
+ value.forEach(push);
169
+ return;
170
+ }
171
+ if (typeof value !== 'string') {
172
+ return;
173
+ }
174
+ value.split(',').forEach((item) => {
175
+ const normalized = item.trim().toLowerCase();
176
+ if (normalized) raw.push(normalized);
177
+ });
178
+ };
179
+ push(params.model);
180
+ push(params.models);
181
+ // API-facing alias: callers may pass modelType when they reuse usage filters
182
+ // outside the CLI flag surface.
183
+ push(params.modelType);
184
+ return [...new Set(raw)];
185
+ }
186
+
187
+ function sessionMatchesUsageExportModelFilters(session, filters) {
188
+ if (!filters.length) {
189
+ return true;
190
+ }
191
+ const models = [];
192
+ if (typeof session.model === 'string') models.push(session.model);
193
+ if (Array.isArray(session.models)) models.push(...session.models.filter(item => typeof item === 'string'));
194
+ const normalizedModels = models.map(item => item.trim().toLowerCase()).filter(Boolean);
195
+ return filters.some(filter => normalizedModels.some(model => model === filter || model.includes(filter)));
196
+ }
197
+
198
+ function escapeUsageCsvCell(value) {
199
+ const raw = value === undefined || value === null ? '' : String(value);
200
+ if (!/[",\n\r]/.test(raw)) {
201
+ return raw;
202
+ }
203
+ return `"${raw.replace(/"/g, '""')}"`;
204
+ }
205
+
206
+ function serializeUsageExportRowsToCsv(rows) {
207
+ const columns = ['date', 'model', 'tokens', 'sessions'];
208
+ const lines = [columns.join(',')];
209
+ for (const row of rows) {
210
+ lines.push(columns.map(column => escapeUsageCsvCell(row[column])).join(','));
211
+ }
212
+ return lines.join('\r\n') + '\r\n';
213
+ }
214
+
215
+ function buildUsageExportRows(sessions = [], params = {}) {
216
+ const fromTime = parseUsageExportDate(params.from ?? params.startDate, 'start');
217
+ const toTime = parseUsageExportDate(params.to ?? params.endDate, 'end');
218
+ if (Number.isNaN(fromTime)) {
219
+ return { error: 'Invalid from date' };
220
+ }
221
+ if (Number.isNaN(toTime)) {
222
+ return { error: 'Invalid to date' };
223
+ }
224
+ if (fromTime !== null && toTime !== null && fromTime >= toTime) {
225
+ return { error: 'from date must be before to date' };
226
+ }
227
+
228
+ const modelFilters = normalizeUsageExportModelFilters(params);
229
+ const groups = new Map();
230
+ for (const session of Array.isArray(sessions) ? sessions : []) {
231
+ if (!session || typeof session !== 'object' || Array.isArray(session)) {
232
+ continue;
233
+ }
234
+ if (!sessionMatchesUsageExportModelFilters(session, modelFilters)) {
235
+ continue;
236
+ }
237
+ const timestamp = Date.parse(session.updatedAt || session.createdAt || '');
238
+ if (!Number.isFinite(timestamp)) {
239
+ continue;
240
+ }
241
+ if (fromTime !== null && timestamp < fromTime) {
242
+ continue;
243
+ }
244
+ if (toTime !== null && timestamp >= toTime) {
245
+ continue;
246
+ }
247
+ const model = typeof session.model === 'string' && session.model.trim()
248
+ ? session.model.trim()
249
+ : (Array.isArray(session.models) && typeof session.models[0] === 'string' ? session.models[0].trim() : 'unknown');
250
+ if (!model) {
251
+ continue;
252
+ }
253
+ const date = formatUsageExportDay(timestamp);
254
+ const key = `${date}\u0000${model}`;
255
+ const current = groups.get(key) || { date, model, tokens: 0, sessions: 0 };
256
+ current.tokens += readNonNegativeInteger(session.totalTokens ?? session.tokens);
257
+ current.sessions += 1;
258
+ groups.set(key, current);
259
+ }
260
+
261
+ const rows = [...groups.values()].sort((a, b) => {
262
+ const dateCompare = a.date.localeCompare(b.date);
263
+ if (dateCompare !== 0) return dateCompare;
264
+ return a.model.localeCompare(b.model);
265
+ });
266
+ return { rows };
267
+ }
268
+
269
+ async function exportSessionUsageCore(params = {}, deps = {}) {
270
+ const listSessionUsage = typeof deps.listSessionUsage === 'function'
271
+ ? deps.listSessionUsage
272
+ : (options) => listSessionUsageCore(options, deps);
273
+ const sessions = Array.isArray(params.sessions)
274
+ ? params.sessions
275
+ : await listSessionUsage({
276
+ source: params.source,
277
+ limit: params.limit,
278
+ forceRefresh: !!params.forceRefresh
279
+ });
280
+ const built = buildUsageExportRows(sessions, params);
281
+ if (built.error) {
282
+ return { error: built.error };
283
+ }
284
+ const format = normalizeUsageExportFormat(params.format);
285
+ const rows = built.rows;
286
+ const content = format === 'json'
287
+ ? JSON.stringify({ rows }, null, 2) + '\n'
288
+ : serializeUsageExportRowsToCsv(rows);
289
+ const extension = format === 'json' ? 'json' : 'csv';
290
+ return {
291
+ format,
292
+ mimeType: format === 'json' ? 'application/json' : 'text/csv',
293
+ fileName: `usage-export.${extension}`,
294
+ rows,
295
+ content
296
+ };
297
+ }
298
+
116
299
  module.exports = {
117
- listSessionUsageCore
300
+ listSessionUsageCore,
301
+ buildUsageExportRows,
302
+ exportSessionUsageCore,
303
+ serializeUsageExportRowsToCsv
118
304
  };
package/cli.js CHANGED
@@ -162,7 +162,8 @@ const {
162
162
  extractSessionDetailPreviewFromTailText,
163
163
  extractSessionDetailPreviewFromFileFast
164
164
  } = require('./lib/cli-sessions');
165
- const { listSessionUsageCore } = require('./cli/session-usage');
165
+ const { listSessionUsageCore, exportSessionUsageCore } = require('./cli/session-usage');
166
+ const { parseAnalyticsExportArgs } = require('./cli/analytics-export-args');
166
167
  const {
167
168
  readBundledWebUiCss,
168
169
  readBundledWebUiHtml,
@@ -5204,6 +5205,12 @@ async function listSessionUsage(params = {}) {
5204
5205
  });
5205
5206
  }
5206
5207
 
5208
+ async function exportSessionUsage(params = {}) {
5209
+ return exportSessionUsageCore(params, {
5210
+ listSessionUsage
5211
+ });
5212
+ }
5213
+
5207
5214
  function listSessionPaths(params = {}) {
5208
5215
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
5209
5216
  if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
@@ -9796,6 +9803,47 @@ async function cmdExportSession(args = []) {
9796
9803
  console.log();
9797
9804
  }
9798
9805
 
9806
+ function printAnalyticsUsage() {
9807
+ console.log('\n用法:');
9808
+ console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|all>] [--output <PATH|->] [-o <PATH|->]');
9809
+ console.log('');
9810
+ }
9811
+
9812
+ async function cmdAnalytics(args = []) {
9813
+ const subcommand = args[0];
9814
+ if (subcommand !== 'export') {
9815
+ printAnalyticsUsage();
9816
+ process.exit(subcommand ? 1 : 0);
9817
+ }
9818
+ const parsed = parseAnalyticsExportArgs(args.slice(1));
9819
+ if (parsed.options.help) {
9820
+ printAnalyticsUsage();
9821
+ process.exit(0);
9822
+ }
9823
+ if (parsed.error) {
9824
+ console.error('错误:', parsed.error);
9825
+ printAnalyticsUsage();
9826
+ process.exit(1);
9827
+ }
9828
+
9829
+ const result = await exportSessionUsage(parsed.options);
9830
+ if (result && result.error) {
9831
+ console.error('导出失败:', result.error);
9832
+ process.exit(1);
9833
+ }
9834
+ const output = parsed.options.output || (result && result.fileName) || `usage-export.${parsed.options.format}`;
9835
+ if (output === '-') {
9836
+ process.stdout.write(result && result.content ? result.content : '');
9837
+ return;
9838
+ }
9839
+ const outputPath = path.resolve(process.cwd(), output);
9840
+ ensureDir(path.dirname(outputPath));
9841
+ fs.writeFileSync(outputPath, result && result.content ? result.content : '', 'utf-8');
9842
+ console.log(`\n✓ Usage 已导出: ${outputPath}`);
9843
+ console.log(` 格式: ${result.format}; rows: ${Array.isArray(result.rows) ? result.rows.length : 0}`);
9844
+ console.log();
9845
+ }
9846
+
9799
9847
  function parseStartOptions(args = []) {
9800
9848
  const options = { host: '', noBrowser: false };
9801
9849
  if (!Array.isArray(args)) {
@@ -11077,6 +11125,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11077
11125
  }
11078
11126
  }
11079
11127
  break;
11128
+ case 'export-sessions-usage':
11129
+ {
11130
+ const usageParams = isPlainObject(params) ? params : {};
11131
+ const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
11132
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
11133
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
11134
+ } else {
11135
+ result = await exportSessionUsage({
11136
+ ...usageParams,
11137
+ source: source || 'all'
11138
+ });
11139
+ }
11140
+ }
11141
+ break;
11080
11142
  case 'list-session-paths':
11081
11143
  {
11082
11144
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
@@ -15960,6 +16022,7 @@ function printMainHelp() {
15960
16022
  console.log(' codexmate delete-model <模型> 删除模型');
15961
16023
  console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
15962
16024
  console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
16025
+ console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--output <PATH|->] [-o <PATH|->] 导出 Usage 数据');
15963
16026
  console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
15964
16027
  console.log(' codexmate update [--check] 检查并快速更新工具');
15965
16028
  console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
@@ -16052,6 +16115,7 @@ async function main() {
16052
16115
  case 'proxy': await cmdProxy(args.slice(1)); break;
16053
16116
  case 'workflow': await cmdWorkflow(args.slice(1)); break;
16054
16117
  case 'task': await cmdTask(args.slice(1)); break;
16118
+ case 'analytics': await cmdAnalytics(args.slice(1)); break;
16055
16119
  case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
16056
16120
  case 'update': await cmdToolUpdate(args.slice(1)); break;
16057
16121
  case 'start':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -31,7 +31,6 @@ document.addEventListener('DOMContentLoaded', () => {
31
31
  const appOptions = {
32
32
  data() {
33
33
  return {
34
- brandHovered: false,
35
34
  lang: 'zh',
36
35
  appVersion: '',
37
36
  mainTab: 'dashboard',
@@ -491,7 +491,7 @@ export function createSkillsMethods({ api }) {
491
491
  try {
492
492
  const res = await api('import-skills', {
493
493
  targetApp: this.skillsTargetApp,
494
- imports: [skill]
494
+ items: [skill]
495
495
  });
496
496
  if (res && res.error) {
497
497
  this.showMessage(res.error, 'error');
@@ -118,11 +118,11 @@
118
118
 
119
119
  <div :class="['app-shell', { standalone: sessionStandalone }]">
120
120
  <aside class="side-rail" v-if="!sessionStandalone">
121
- <div class="brand-block" tabindex="0" @mouseenter="brandHovered = true" @mouseleave="brandHovered = false" @focus="brandHovered = true" @blur="brandHovered = false">
121
+ <div class="brand-block">
122
122
  <div class="brand-head">
123
123
  <img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
124
124
  <div class="brand-copy">
125
- <div class="brand-kicker">Codex Mate<transition name="brand-version-fade"><span v-if="appVersion && brandHovered" class="brand-version"> v{{ appVersion }}</span></transition></div>
125
+ <div class="brand-kicker">Codex Mate<span v-if="appVersion" class="brand-version"> v{{ appVersion }}</span></div>
126
126
  </div>
127
127
  </div>
128
128
  </div>
@@ -315,6 +315,10 @@
315
315
  </div>
316
316
  </button>
317
317
  </div>
318
+ <div id="side-tab-new" class="side-item side-item-ghost" tabindex="-1" aria-hidden="true">
319
+ <div class="side-item-title">New Tab</div>
320
+ <div class="side-item-meta"><span>&nbsp;</span></div>
321
+ </div>
318
322
  </div>
319
323
 
320
324
  <div class="side-rail-lang" role="group" :aria-label="t('lang.label')">
@@ -1,4 +1,4 @@
1
- <!-- Usage 统计 - 时光之河设计 -->
1
+ <!-- Usage 统计 - 流光设计 -->
2
2
  <div
3
3
  v-show="mainTab === 'usage'"
4
4
  class="mode-content"
@@ -71,24 +71,20 @@
71
71
  </div>
72
72
  </div>
73
73
 
74
- <!-- 波浪图:替换柱状图 -->
75
- <section v-if="sessionUsageWave.points && sessionUsageWave.points.length" class="usage-card usage-wave-section">
74
+ <!-- 波浪图 -->
75
+ <section v-if="sessionUsageWave.points && sessionUsageWave.points.length" class="usage-wave-section">
76
76
  <div class="usage-card-title">{{ t('usage.daily.title') }}</div>
77
77
  <div class="usage-wave-container">
78
78
  <svg class="usage-wave-chart" viewBox="0 0 800 140" preserveAspectRatio="none">
79
79
  <defs>
80
80
  <linearGradient :id="'wave-gradient-' + sessionsUsageTimeRange" x1="0" y1="0" x2="0" y2="1">
81
- <stop offset="0%" :stop-color="'var(--color-brand)'" stop-opacity="0.3"/>
81
+ <stop offset="0%" :stop-color="'var(--color-brand)'" stop-opacity="0.35"/>
82
82
  <stop offset="100%" :stop-color="'var(--color-brand)'" stop-opacity="0"/>
83
83
  </linearGradient>
84
84
  </defs>
85
- <!-- 填充区域 -->
86
85
  <path :d="sessionUsageWave.areaPath" :fill="'url(#wave-gradient-' + sessionsUsageTimeRange + ')'" class="usage-wave-area"/>
87
- <!-- 曲线 -->
88
86
  <path :d="sessionUsageWave.linePath" fill="none" :stroke="'var(--color-brand)'" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="usage-wave-line"/>
89
- <!-- 悬停指示线 -->
90
- <line v-if="sessionsUsageSelectedDay" x1="0" :x2="sessionUsageWave.width" :y1="sessionUsageWave.hoverY" :y2="sessionUsageWave.hoverY" stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.4" class="usage-wave-hover-line"/>
91
- <!-- 悬停点 -->
87
+ <line v-if="sessionsUsageSelectedDay" x1="0" :x2="sessionUsageWave.width" :y1="sessionUsageWave.hoverY" :y2="sessionUsageWave.hoverY" stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.5" class="usage-wave-hover-line"/>
92
88
  <circle v-if="sessionsUsageSelectedDay" :cx="sessionUsageWave.hoverX" :cy="sessionUsageWave.hoverY" r="5" :fill="'var(--color-surface)'" :stroke="'var(--color-brand)'" stroke-width="2.5" class="usage-wave-hover-point"/>
93
89
  </svg>
94
90
  <div class="usage-wave-labels">
@@ -112,8 +108,8 @@
112
108
  </section>
113
109
 
114
110
  <div class="usage-chart-grid">
115
- <!-- 热力图:垂直活动条 -->
116
- <section class="usage-card usage-hourly-heatmap">
111
+ <!-- 热力图 -->
112
+ <section class="usage-card-hourly-heatmap">
117
113
  <div class="usage-card-title">{{ t('usage.hourlyHeatmap.title') }}</div>
118
114
  <div class="hourly-heatmap-wrapper">
119
115
  <div class="hourly-heatmap-header">
@@ -166,7 +162,7 @@
166
162
  </section>
167
163
 
168
164
  <!-- Top Paths -->
169
- <section class="usage-card usage-paths-section">
165
+ <section class="usage-paths-section">
170
166
  <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
171
167
  <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>
172
168
  <div v-else class="usage-list-paths">