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 +14 -5
- package/README.zh.md +14 -5
- package/cli/analytics-export-args.js +68 -0
- package/cli/session-usage.js +187 -1
- package/cli.js +65 -1
- package/package.json +1 -1
- package/web-ui/app.js +0 -1
- package/web-ui/modules/skills.methods.mjs +1 -1
- package/web-ui/partials/index/layout-header.html +6 -2
- package/web-ui/partials/index/panel-usage.html +8 -12
- package/web-ui/res/web-ui-render.precompiled.js +29 -34
- package/web-ui/styles/layout-shell.css +11 -12
- package/web-ui/styles/sessions-usage.css +299 -151
- package/web-ui/partials/index/panel-config-codex.html.bak +0 -337
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
[](https://www.npmjs.com/package/codexmate)
|
|
16
16
|
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
17
17
|
[](https://www.npmjs.com/package/codexmate)
|
|
18
|
+
[](#install-via-homebrew-macos--linux)
|
|
18
19
|
[](#quick-start)
|
|
19
20
|
[](https://nodejs.org/)
|
|
20
21
|
[](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
|
[](https://www.npmjs.com/package/codexmate)
|
|
16
16
|
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
17
17
|
[](https://www.npmjs.com/package/codexmate)
|
|
18
|
+
[](#homebrew-安装macos--linux)
|
|
18
19
|
[](#快速开始)
|
|
19
20
|
[](https://nodejs.org/)
|
|
20
21
|
[](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
|
+
};
|
package/cli/session-usage.js
CHANGED
|
@@ -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
package/web-ui/app.js
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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<
|
|
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> </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-
|
|
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.
|
|
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
|
|
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-
|
|
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">
|