codexmate 0.0.37 → 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.
|
@@ -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
|
@@ -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');
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
<!-- Provider 配置模式(Codex) -->
|
|
2
|
-
<div
|
|
3
|
-
v-show="mainTab === 'config' && isProviderConfigMode"
|
|
4
|
-
class="mode-content mode-cards"
|
|
5
|
-
id="panel-config-provider"
|
|
6
|
-
role="tabpanel"
|
|
7
|
-
:aria-labelledby="forceCompactLayout ? 'tab-config' : ('side-tab-config-' + configMode)">
|
|
8
|
-
<div v-if="forceCompactLayout && !sessionStandalone" class="segmented-control">
|
|
9
|
-
<button type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
|
|
10
|
-
<button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
|
|
11
|
-
<button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
|
|
12
|
-
</div>
|
|
13
|
-
<template v-if="isCodexConfigMode && shouldShowCliInstallPlaceholder('codex')">
|
|
14
|
-
<div class="selector-section">
|
|
15
|
-
<div class="empty-state">
|
|
16
|
-
<div class="empty-state-title">{{ t('cli.missing.title', { name: 'Codex' }) }}</div>
|
|
17
|
-
<div class="empty-state-subtitle">{{ t('cli.missing.subtitle', { name: 'Codex' }) }}</div>
|
|
18
|
-
<div class="docs-command-row">
|
|
19
|
-
<div class="docs-command-box" role="group" :aria-label="t('cli.missing.commandAria', { name: 'Codex' })">
|
|
20
|
-
<code class="install-command">{{ getInstallCommand('codex', 'install') }}</code>
|
|
21
|
-
<button
|
|
22
|
-
type="button"
|
|
23
|
-
class="btn-mini docs-copy-btn"
|
|
24
|
-
:disabled="!getInstallCommand('codex', 'install')"
|
|
25
|
-
@click="copyInstallCommand(getInstallCommand('codex', 'install'))">{{ t('common.copy') }}</button>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
<button type="button" class="btn-tool btn-tool-compact" @click="mainTab = 'docs'; setInstallCommandAction('install')">{{ t('cli.missing.openDocs') }}</button>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</template>
|
|
32
|
-
<template v-else>
|
|
33
|
-
<!-- 添加提供商按钮 -->
|
|
34
|
-
<button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
|
|
35
|
-
<svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
|
|
36
|
-
<path d="M10 4v12M4 10h12"/>
|
|
37
|
-
</svg>
|
|
38
|
-
{{ t('config.addProvider') }}
|
|
39
|
-
</button>
|
|
40
|
-
|
|
41
|
-
<!-- 服务预设 -->
|
|
42
|
-
<div class="selector-section" v-if="isCodexConfigMode && codexProviderTemplates.length">
|
|
43
|
-
<div class="selector-header">
|
|
44
|
-
<span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
|
|
47
|
-
<button
|
|
48
|
-
v-for="tpl in codexProviderTemplates"
|
|
49
|
-
:key="tpl.name"
|
|
50
|
-
type="button"
|
|
51
|
-
class="btn-mini"
|
|
52
|
-
@click="newProvider.name = tpl.name;
|
|
53
|
-
newProvider.url = tpl.url;
|
|
54
|
-
newProvider._suggestedModel = tpl.model || '';
|
|
55
|
-
newProvider.useTransform = !!tpl.useTransform;
|
|
56
|
-
showAddModal = true">
|
|
57
|
-
{{ tpl.label }}
|
|
58
|
-
</button>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<!-- 模型选择器 -->
|
|
63
|
-
<div class="selector-section">
|
|
64
|
-
<div class="selector-header">
|
|
65
|
-
<span class="selector-title">{{ t('config.models') }}</span>
|
|
66
|
-
<div class="selector-actions">
|
|
67
|
-
<button class="btn-icon" @click="showModelModal = true" :aria-label="t('modal.modelAdd.title')" :title="t('modal.modelAdd.title')" v-if="modelsSource === 'legacy'">+</button>
|
|
68
|
-
<button class="btn-icon" @click="showModelListModal = true" :aria-label="t('modal.modelManage.title')" :title="t('modal.modelManage.title')" v-if="modelsSource === 'legacy'">≡</button>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
<select
|
|
72
|
-
v-if="codexModelsLoading || modelsSource === 'remote'"
|
|
73
|
-
class="model-select"
|
|
74
|
-
v-model="currentModel"
|
|
75
|
-
@change="onModelChange"
|
|
76
|
-
:disabled="codexModelsLoading"
|
|
77
|
-
>
|
|
78
|
-
<option v-if="codexModelsLoading" value="">{{ t('config.modelLoading') }}</option>
|
|
79
|
-
<option v-else v-for="model in codexModelOptions" :key="model" :value="model">{{ model }}</option>
|
|
80
|
-
</select>
|
|
81
|
-
<input
|
|
82
|
-
v-if="!codexModelsLoading && (modelsSource !== 'remote' || !modelsHasCurrent)"
|
|
83
|
-
class="model-input"
|
|
84
|
-
v-model="currentModel"
|
|
85
|
-
@blur="onModelChange"
|
|
86
|
-
@keyup.enter="onModelChange"
|
|
87
|
-
:placeholder="activeProviderModelPlaceholder"
|
|
88
|
-
:list="codexModelHasList ? 'codex-model-options' : null"
|
|
89
|
-
>
|
|
90
|
-
<datalist v-if="codexModelHasList" id="codex-model-options">
|
|
91
|
-
<option v-for="model in codexModelOptions" :key="model" :value="model"></option>
|
|
92
|
-
</datalist>
|
|
93
|
-
<div class="config-template-hint" v-if="modelsSource === 'unlimited'">
|
|
94
|
-
{{ t('config.models.unlimited') }}
|
|
95
|
-
</div>
|
|
96
|
-
<div class="config-template-hint" v-if="modelsSource === 'error'">
|
|
97
|
-
{{ t('config.models.error') }}
|
|
98
|
-
</div>
|
|
99
|
-
<div class="config-template-hint" v-if="modelsSource === 'remote' && !modelsHasCurrent">
|
|
100
|
-
{{ isCodexConfigMode ? t('config.models.notInList.codex') : t('config.models.notInList.other') }}
|
|
101
|
-
</div>
|
|
102
|
-
<div class="config-template-hint" v-if="isCodexConfigMode">
|
|
103
|
-
{{ t('config.template.editFirst') }}
|
|
104
|
-
</div>
|
|
105
|
-
<div class="config-template-hint" v-else-if="activeProviderBridgeHint">
|
|
106
|
-
{{ t('config.template.bridgeCodexOnly', { hint: activeProviderBridgeHint }) }}
|
|
107
|
-
</div>
|
|
108
|
-
<button class="btn-tool btn-template-editor" v-if="isCodexConfigMode" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
|
|
109
|
-
{{ t('config.template.openEditor') }}
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<template v-if="isCodexConfigMode">
|
|
114
|
-
<div class="selector-section">
|
|
115
|
-
<div class="selector-header">
|
|
116
|
-
<span class="selector-title">{{ t('config.serviceTier') }}</span>
|
|
117
|
-
</div>
|
|
118
|
-
<select class="model-select" v-model="serviceTier" @change="onServiceTierChange">
|
|
119
|
-
<option value="fast">{{ t('config.serviceTier.fast') }}</option>
|
|
120
|
-
<option value="standard">{{ t('config.serviceTier.standard') }}</option>
|
|
121
|
-
</select>
|
|
122
|
-
<div class="config-template-hint">
|
|
123
|
-
{{ t('config.serviceTier.hint', { field: 'service_tier' }) }}
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<div class="selector-section">
|
|
128
|
-
<div class="selector-header">
|
|
129
|
-
<span class="selector-title">{{ t('config.reasoningEffort') }}</span>
|
|
130
|
-
</div>
|
|
131
|
-
<select class="model-select" v-model="modelReasoningEffort" @change="onReasoningEffortChange">
|
|
132
|
-
<option value="high">high</option>
|
|
133
|
-
<option value="medium">{{ t('config.reasoningEffort.medium') }}</option>
|
|
134
|
-
<option value="low">low</option>
|
|
135
|
-
<option value="xhigh">xhigh</option>
|
|
136
|
-
</select>
|
|
137
|
-
<div class="config-template-hint">
|
|
138
|
-
{{ t('config.reasoningEffort.hint') }}
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<div class="selector-section">
|
|
143
|
-
<div class="selector-header">
|
|
144
|
-
<span class="selector-title">{{ t('config.contextBudget') }}</span>
|
|
145
|
-
<div class="selector-actions">
|
|
146
|
-
<button
|
|
147
|
-
class="btn-tool btn-tool-compact"
|
|
148
|
-
@click="resetCodexContextBudgetDefaults"
|
|
149
|
-
:disabled="loading || !!initError || codexApplying">
|
|
150
|
-
{{ t('config.reset') }}
|
|
151
|
-
</button>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div class="codex-config-grid">
|
|
155
|
-
<div class="form-group codex-config-field">
|
|
156
|
-
<label class="form-label" for="codex-model-context-window">model_context_window</label>
|
|
157
|
-
<input
|
|
158
|
-
id="codex-model-context-window"
|
|
159
|
-
v-model="modelContextWindowInput"
|
|
160
|
-
class="form-input"
|
|
161
|
-
inputmode="numeric"
|
|
162
|
-
autocomplete="off"
|
|
163
|
-
:placeholder="t('config.example', { value: 190000 })"
|
|
164
|
-
@focus="editingCodexBudgetField = 'modelContextWindowInput'"
|
|
165
|
-
@input="sanitizePositiveIntegerDraft('modelContextWindowInput')"
|
|
166
|
-
@blur="onModelContextWindowBlur"
|
|
167
|
-
@keydown.enter.prevent="onModelContextWindowBlur">
|
|
168
|
-
<div class="form-hint">{{ t('config.contextWindow.hint') }}</div>
|
|
169
|
-
</div>
|
|
170
|
-
<div class="form-group codex-config-field">
|
|
171
|
-
<label class="form-label" for="codex-model-auto-compact-token-limit">model_auto_compact_token_limit</label>
|
|
172
|
-
<input
|
|
173
|
-
id="codex-model-auto-compact-token-limit"
|
|
174
|
-
v-model="modelAutoCompactTokenLimitInput"
|
|
175
|
-
class="form-input"
|
|
176
|
-
inputmode="numeric"
|
|
177
|
-
autocomplete="off"
|
|
178
|
-
:placeholder="t('config.example', { value: 185000 })"
|
|
179
|
-
@focus="editingCodexBudgetField = 'modelAutoCompactTokenLimitInput'"
|
|
180
|
-
@input="sanitizePositiveIntegerDraft('modelAutoCompactTokenLimitInput')"
|
|
181
|
-
@blur="onModelAutoCompactTokenLimitBlur"
|
|
182
|
-
@keydown.enter.prevent="onModelAutoCompactTokenLimitBlur">
|
|
183
|
-
<div class="form-hint">{{ t('config.autoCompact.hint') }}</div>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
<div class="selector-section">
|
|
189
|
-
<div class="selector-header">
|
|
190
|
-
<span class="selector-title">AGENTS.md</span>
|
|
191
|
-
</div>
|
|
192
|
-
<button class="btn-tool" @click="openAgentsEditor" :disabled="loading || !!initError || agentsLoading">
|
|
193
|
-
{{ agentsLoading ? t('config.modelLoading') : t('config.agents.open') }}
|
|
194
|
-
</button>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
<div class="selector-section">
|
|
198
|
-
<div class="selector-header">
|
|
199
|
-
<span class="selector-title">{{ t('config.health.title') }}</span>
|
|
200
|
-
</div>
|
|
201
|
-
<button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
|
|
202
|
-
{{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}
|
|
203
|
-
</button>
|
|
204
|
-
<div class="config-template-hint">{{ t('config.health.hint') }}</div>
|
|
205
|
-
<div v-if="healthCheckLoading && healthCheckBatchTotal" class="config-template-hint">
|
|
206
|
-
{{ t('config.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }}
|
|
207
|
-
</div>
|
|
208
|
-
<div v-if="healthCheckResult && !healthCheckLoading" class="config-template-hint">
|
|
209
|
-
{{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }}
|
|
210
|
-
</div>
|
|
211
|
-
<button v-if="healthCheckResult && !healthCheckLoading" type="button" class="btn-mini" @click="showHealthCheckModal = true">
|
|
212
|
-
{{ t('common.detail') }}
|
|
213
|
-
</button>
|
|
214
|
-
<div v-if="healthCheckResult && !healthCheckLoading && (healthCheckResult.issues || []).length">
|
|
215
|
-
<div v-for="(issue, index) in healthCheckResult.issues" :key="issue.code || ('issue-' + index)" class="config-template-hint">
|
|
216
|
-
{{ issue.message || issue.code || '' }}<span v-if="issue.suggestion"> · {{ issue.suggestion }}</span>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
|
|
221
|
-
</template>
|
|
222
|
-
|
|
223
|
-
<div v-if="!loading && !initError" class="card-list">
|
|
224
|
-
<div v-for="provider in displayProvidersList" :key="provider.name"
|
|
225
|
-
:class="['card', { active: displayCurrentProvider === provider.name }]"
|
|
226
|
-
@click="switchProvider(provider.name)"
|
|
227
|
-
@keydown.enter.self.prevent="switchProvider(provider.name)"
|
|
228
|
-
@keydown.space.self.prevent="switchProvider(provider.name)"
|
|
229
|
-
tabindex="0"
|
|
230
|
-
role="button"
|
|
231
|
-
:aria-current="displayCurrentProvider === provider.name ? 'true' : null">
|
|
232
|
-
<div class="card-leading">
|
|
233
|
-
<div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
|
|
234
|
-
<div class="card-content">
|
|
235
|
-
<div class="card-title">
|
|
236
|
-
<span>{{ provider.name }}</span>
|
|
237
|
-
<span v-if="provider.readOnly" class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
|
|
238
|
-
</div>
|
|
239
|
-
<div v-if="provider.name !== 'local'" class="card-subtitle card-subtitle-model">
|
|
240
|
-
{{ activeProviderModel(provider.name) || t('config.model.unset') }}
|
|
241
|
-
</div>
|
|
242
|
-
<div v-if="provider.name !== 'local'" class="card-subtitle card-subtitle-url">
|
|
243
|
-
{{ displayProviderUrl(provider) || t('config.url.unset') }}
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
<div class="card-trailing">
|
|
248
|
-
<span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">
|
|
249
|
-
{{ formatLatency(speedResults[provider.name]) }}
|
|
250
|
-
</span>
|
|
251
|
-
<span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">
|
|
252
|
-
{{ providerPillText(provider) }}
|
|
253
|
-
</span>
|
|
254
|
-
<div class="card-actions" @click.stop>
|
|
255
|
-
<button
|
|
256
|
-
class="card-action-btn"
|
|
257
|
-
:class="{ loading: speedLoading[provider.name] }"
|
|
258
|
-
:disabled="!!speedLoading[provider.name]"
|
|
259
|
-
@click="runSpeedTest(provider.name, { silent: true })"
|
|
260
|
-
:aria-label="t('config.availabilityTestAria', { name: provider.name })"
|
|
261
|
-
:title="t('config.availabilityTest')"
|
|
262
|
-
>
|
|
263
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
264
|
-
<path d="M13 2L3 14h7l-1 8 12-14h-7l1-6z"/>
|
|
265
|
-
</svg>
|
|
266
|
-
</button>
|
|
267
|
-
<button
|
|
268
|
-
v-if="!provider.readOnly"
|
|
269
|
-
class="card-action-btn"
|
|
270
|
-
:class="{ loading: providerShareLoading[provider.name], disabled: !shouldAllowProviderShare(provider) }"
|
|
271
|
-
:disabled="providerShareLoading[provider.name] || !shouldAllowProviderShare(provider)"
|
|
272
|
-
@click="copyProviderShareCommand(provider)"
|
|
273
|
-
:title="shouldAllowProviderShare(provider) ? t('config.shareCommand') : t('config.shareDisabled')"
|
|
274
|
-
:aria-label="t('config.shareCommand.aria')">
|
|
275
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
276
|
-
<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
|
|
277
|
-
<path d="M16 6l-4-4-4 4"/>
|
|
278
|
-
<path d="M12 2v14"/>
|
|
279
|
-
</svg>
|
|
280
|
-
</button>
|
|
281
|
-
<button
|
|
282
|
-
v-if="!provider.readOnly"
|
|
283
|
-
class="card-action-btn"
|
|
284
|
-
:class="{ disabled: !shouldShowProviderEdit(provider) }"
|
|
285
|
-
:disabled="!shouldShowProviderEdit(provider)"
|
|
286
|
-
@click="openEditModal(provider)"
|
|
287
|
-
:aria-label="t('config.provider.edit.aria', { name: provider.name })"
|
|
288
|
-
:title="shouldShowProviderEdit(provider) ? t('common.edit') : t('common.notEditable')">
|
|
289
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
290
|
-
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
|
291
|
-
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
292
|
-
</svg>
|
|
293
|
-
</button>
|
|
294
|
-
<button
|
|
295
|
-
v-if="!provider.readOnly"
|
|
296
|
-
class="card-action-btn"
|
|
297
|
-
@click="openCloneProviderModal(provider)"
|
|
298
|
-
:aria-label="t('config.provider.clone.aria', { name: provider.name })"
|
|
299
|
-
:title="t('config.provider.clone')">
|
|
300
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
301
|
-
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
302
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
303
|
-
</svg>
|
|
304
|
-
</button>
|
|
305
|
-
<button
|
|
306
|
-
v-if="!provider.readOnly"
|
|
307
|
-
class="card-action-btn delete"
|
|
308
|
-
:class="{ disabled: !shouldShowProviderDelete(provider) }"
|
|
309
|
-
:disabled="!shouldShowProviderDelete(provider)"
|
|
310
|
-
@click="deleteProvider(provider.name)"
|
|
311
|
-
:aria-label="t('config.provider.delete.aria', { name: provider.name })"
|
|
312
|
-
:title="shouldShowProviderDelete(provider) ? t('common.delete') : t('common.notDeletable')">
|
|
313
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
314
|
-
<path d="M3 6h18"/>
|
|
315
|
-
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
|
316
|
-
</svg>
|
|
317
|
-
</button>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
<div v-if="displayCurrentProvider === 'local'" class="local-bridge-panel" style="margin-top:12px;padding:12px;background:var(--card-bg);border-radius:8px;border:1px solid var(--border-color)">
|
|
324
|
-
<div style="font-size:13px;font-weight:600;margin-bottom:8px;color:var(--text-secondary)">轮询池 — 勾选参与负载均衡的提供商</div>
|
|
325
|
-
<div v-if="localBridgeCandidateProviders().length === 0" style="font-size:12px;color:var(--text-muted)">暂无可用上游 provider,请先添加直连 provider</div>
|
|
326
|
-
<label v-for="cp in localBridgeCandidateProviders()" :key="cp.name"
|
|
327
|
-
style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;font-size:13px">
|
|
328
|
-
<input type="checkbox"
|
|
329
|
-
:checked="!isLocalBridgeExcluded(cp.name)"
|
|
330
|
-
@change="toggleLocalBridgeExcluded(cp.name)"
|
|
331
|
-
style="accent-color:var(--accent-color)" />
|
|
332
|
-
<span>{{ cp.name }}</span>
|
|
333
|
-
</label>
|
|
334
|
-
</div>
|
|
335
|
-
|
|
336
|
-
</template>
|
|
337
|
-
</div>
|