codexmate 0.0.31 → 0.0.33
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 +92 -308
- package/README.zh.md +94 -318
- package/cli/local-bridge.js +227 -0
- package/cli/update.js +162 -0
- package/cli.js +357 -112
- package/lib/cli-sessions.js +16 -6
- package/lib/win-tray.js +119 -0
- package/package.json +2 -2
- package/web-ui/app.js +4 -0
- package/web-ui/logic.sessions.mjs +17 -1
- package/web-ui/modules/app.computed.session.mjs +51 -315
- package/web-ui/modules/app.methods.agents.mjs +19 -0
- package/web-ui/modules/app.methods.claude-config.mjs +71 -2
- package/web-ui/modules/app.methods.codex-config.mjs +20 -0
- package/web-ui/modules/app.methods.providers.mjs +53 -7
- package/web-ui/modules/app.methods.session-actions.mjs +1 -1
- package/web-ui/modules/app.methods.session-browser.mjs +29 -1
- package/web-ui/modules/app.methods.startup-claude.mjs +4 -0
- package/web-ui/modules/i18n.dict.mjs +21 -3
- package/web-ui/partials/index/layout-header.html +1 -2
- package/web-ui/partials/index/modal-config-template-agents.html +12 -1
- package/web-ui/partials/index/modals-basic.html +14 -3
- package/web-ui/partials/index/panel-config-claude.html +57 -85
- package/web-ui/partials/index/panel-config-codex.html +60 -226
- package/web-ui/partials/index/panel-dashboard.html +0 -33
- package/web-ui/partials/index/panel-docs.html +21 -53
- package/web-ui/partials/index/panel-sessions.html +37 -20
- package/web-ui/partials/index/panel-trash.html +33 -38
- package/web-ui/partials/index/panel-usage.html +71 -304
- package/web-ui/styles/controls-forms.css +11 -0
- package/web-ui/styles/docs-panel.css +57 -83
- package/web-ui/styles/layout-shell.css +26 -24
- package/web-ui/styles/modals-core.css +33 -0
- package/web-ui/styles/responsive.css +5 -67
- package/web-ui/styles/sessions-list.css +274 -8
- package/web-ui/styles/sessions-toolbar-trash.css +185 -15
- package/web-ui/styles/sessions-usage.css +336 -788
|
@@ -27,53 +27,16 @@ function formatCompactUsageSummaryNumber(value) {
|
|
|
27
27
|
}).format(Math.floor(numeric));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function readUsageCostNumber(value) {
|
|
31
|
-
const numeric = Number(value);
|
|
32
|
-
if (!Number.isFinite(numeric) || numeric < 0) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return numeric;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function formatUsageEstimatedCost(value, options = {}) {
|
|
39
|
-
const numeric = Number(value);
|
|
40
|
-
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
41
|
-
return '$0.00';
|
|
42
|
-
}
|
|
43
|
-
const precise = options && options.precise === true;
|
|
44
|
-
if (numeric < 0.0001) {
|
|
45
|
-
return '<$0.0001';
|
|
46
|
-
}
|
|
47
|
-
let fractionDigits = 2;
|
|
48
|
-
if (numeric < 1) {
|
|
49
|
-
fractionDigits = precise ? 6 : 4;
|
|
50
|
-
} else if (numeric >= 100) {
|
|
51
|
-
fractionDigits = 0;
|
|
52
|
-
}
|
|
53
|
-
return new Intl.NumberFormat('en-US', {
|
|
54
|
-
style: 'currency',
|
|
55
|
-
currency: 'USD',
|
|
56
|
-
minimumFractionDigits: fractionDigits,
|
|
57
|
-
maximumFractionDigits: fractionDigits
|
|
58
|
-
}).format(numeric);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
30
|
function formatSignedUsageSummaryNumber(value) {
|
|
62
31
|
const numeric = Number(value);
|
|
63
|
-
if (!Number.isFinite(numeric)
|
|
32
|
+
if (!Number.isFinite(numeric)) {
|
|
64
33
|
return '0';
|
|
65
34
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const numeric = Number(value);
|
|
72
|
-
if (!Number.isFinite(numeric) || numeric === 0) {
|
|
73
|
-
return '$0.00';
|
|
74
|
-
}
|
|
75
|
-
const sign = numeric > 0 ? '+' : '-';
|
|
76
|
-
return `${sign}${formatUsageEstimatedCost(Math.abs(numeric), options)}`;
|
|
35
|
+
const abs = Math.floor(Math.abs(numeric));
|
|
36
|
+
const formatted = abs.toLocaleString('en-US');
|
|
37
|
+
if (numeric > 0) return `+${formatted}`;
|
|
38
|
+
if (numeric < 0) return `-${formatted}`;
|
|
39
|
+
return '0';
|
|
77
40
|
}
|
|
78
41
|
|
|
79
42
|
function formatUsageRangeLabel(range, t) {
|
|
@@ -149,208 +112,20 @@ function formatUsageDuration(value, options = {}) {
|
|
|
149
112
|
return parts.length ? parts.join(compact ? '' : ' ') : (isEn ? '0m' : '0分');
|
|
150
113
|
}
|
|
151
114
|
|
|
152
|
-
const KNOWN_USAGE_MODEL_PRICING = Object.freeze({
|
|
153
|
-
'gpt-5.4': Object.freeze({ input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }),
|
|
154
|
-
'gpt-5.4-mini': Object.freeze({ input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }),
|
|
155
|
-
'gpt-5.3-codex': Object.freeze({ input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }),
|
|
156
|
-
'gpt-5.2-codex': Object.freeze({ input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }),
|
|
157
|
-
'claude-opus-4-6': Object.freeze({ input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, reasoningOutput: 75 }),
|
|
158
|
-
'claude-opus-4-7': Object.freeze({ input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, reasoningOutput: 75 }),
|
|
159
|
-
'claude-sonnet-4-6': Object.freeze({ input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, reasoningOutput: 15 }),
|
|
160
|
-
'claude-haiku-4-5': Object.freeze({ input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, reasoningOutput: 4 }),
|
|
161
|
-
'claude-3-5-sonnet': Object.freeze({ input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, reasoningOutput: 15 }),
|
|
162
|
-
'claude-3-5-haiku': Object.freeze({ input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, reasoningOutput: 4 }),
|
|
163
|
-
'claude-3-opus': Object.freeze({ input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, reasoningOutput: 75 })
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
function createUsagePricingEntry(pricing, source) {
|
|
167
|
-
const resolvedSource = typeof source === 'string' && source.trim() ? source.trim() : 'provider-config';
|
|
168
|
-
return {
|
|
169
|
-
input: readUsageCostNumber(pricing && pricing.input),
|
|
170
|
-
output: readUsageCostNumber(pricing && pricing.output),
|
|
171
|
-
reasoningOutput: readUsageCostNumber(
|
|
172
|
-
pricing && (pricing.reasoningOutput != null ? pricing.reasoningOutput : pricing.reasoning)
|
|
173
|
-
),
|
|
174
|
-
cacheRead: readUsageCostNumber(pricing && pricing.cacheRead),
|
|
175
|
-
cacheWrite: readUsageCostNumber(pricing && pricing.cacheWrite),
|
|
176
|
-
source: resolvedSource
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function buildUsagePricingIndex(providersList = []) {
|
|
181
|
-
const byProvider = new Map();
|
|
182
|
-
const byModel = new Map();
|
|
183
|
-
const knownByModel = new Map();
|
|
184
|
-
const list = Array.isArray(providersList) ? providersList : [];
|
|
185
|
-
for (const provider of list) {
|
|
186
|
-
if (!provider || typeof provider !== 'object') continue;
|
|
187
|
-
const providerName = typeof provider.name === 'string' ? provider.name.trim() : '';
|
|
188
|
-
const models = Array.isArray(provider.models) ? provider.models : [];
|
|
189
|
-
const providerMap = new Map();
|
|
190
|
-
for (const model of models) {
|
|
191
|
-
if (!model || typeof model !== 'object') continue;
|
|
192
|
-
const modelId = typeof model.id === 'string' ? model.id.trim() : '';
|
|
193
|
-
if (!modelId) continue;
|
|
194
|
-
const pricing = createUsagePricingEntry(
|
|
195
|
-
model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
|
|
196
|
-
? model.cost
|
|
197
|
-
: null,
|
|
198
|
-
'provider-config'
|
|
199
|
-
);
|
|
200
|
-
const hasKnownRate = [pricing.input, pricing.output, pricing.reasoningOutput, pricing.cacheRead, pricing.cacheWrite]
|
|
201
|
-
.some((value) => value !== null);
|
|
202
|
-
if (!hasKnownRate) continue;
|
|
203
|
-
providerMap.set(modelId, pricing);
|
|
204
|
-
const modelMatches = byModel.get(modelId) || [];
|
|
205
|
-
modelMatches.push({ provider: providerName, pricing });
|
|
206
|
-
byModel.set(modelId, modelMatches);
|
|
207
|
-
}
|
|
208
|
-
if (providerName && providerMap.size) {
|
|
209
|
-
byProvider.set(providerName, providerMap);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
for (const [modelId, pricing] of Object.entries(KNOWN_USAGE_MODEL_PRICING)) {
|
|
213
|
-
const normalizedModelId = typeof modelId === 'string' ? modelId.trim() : '';
|
|
214
|
-
if (!normalizedModelId || byModel.has(normalizedModelId)) {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
knownByModel.set(normalizedModelId, createUsagePricingEntry(pricing, 'public-catalog'));
|
|
218
|
-
}
|
|
219
|
-
return { byProvider, byModel, knownByModel };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function resolveUsagePricingForSession(session, pricingIndex, fallbackProvider = '') {
|
|
223
|
-
if (!session || typeof session !== 'object' || !pricingIndex || typeof pricingIndex !== 'object') {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
const model = typeof session.model === 'string' ? session.model.trim() : '';
|
|
227
|
-
if (!model) return null;
|
|
228
|
-
const provider = typeof session.provider === 'string' ? session.provider.trim() : '';
|
|
229
|
-
const effectiveProvider = provider || fallbackProvider;
|
|
230
|
-
if (effectiveProvider) {
|
|
231
|
-
const providerMap = pricingIndex.byProvider instanceof Map ? pricingIndex.byProvider.get(effectiveProvider) : null;
|
|
232
|
-
if (providerMap instanceof Map && providerMap.has(model)) {
|
|
233
|
-
return providerMap.get(model);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const modelMatches = pricingIndex.byModel instanceof Map ? pricingIndex.byModel.get(model) : null;
|
|
237
|
-
if (Array.isArray(modelMatches) && modelMatches.length === 1) {
|
|
238
|
-
return modelMatches[0].pricing;
|
|
239
|
-
}
|
|
240
|
-
const knownPricing = pricingIndex.knownByModel instanceof Map ? pricingIndex.knownByModel.get(model) : null;
|
|
241
|
-
if (knownPricing) {
|
|
242
|
-
return knownPricing;
|
|
243
|
-
}
|
|
244
|
-
const strippedModel = model.replace(/-\d{8}(?=[-_]|$)/, '');
|
|
245
|
-
if (strippedModel !== model) {
|
|
246
|
-
const strippedKnown = pricingIndex.knownByModel instanceof Map ? pricingIndex.knownByModel.get(strippedModel) : null;
|
|
247
|
-
if (strippedKnown) {
|
|
248
|
-
return strippedKnown;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function shouldEstimateUsageCostForSession() {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function estimateUsageCostSummary(sessions, providersList, currentProvider) {
|
|
259
|
-
const list = Array.isArray(sessions) ? sessions : [];
|
|
260
|
-
const pricingIndex = buildUsagePricingIndex(providersList);
|
|
261
|
-
let totalCostUsd = 0;
|
|
262
|
-
let estimatedSessions = 0;
|
|
263
|
-
let totalTokens = 0;
|
|
264
|
-
let estimatedTokens = 0;
|
|
265
|
-
let configuredSessions = 0;
|
|
266
|
-
let catalogSessions = 0;
|
|
267
|
-
let missingPricingSessions = 0;
|
|
268
|
-
let missingTokenSessions = 0;
|
|
269
|
-
let supportedSessions = 0;
|
|
270
|
-
let skippedUnsupportedSessions = 0;
|
|
271
|
-
|
|
272
|
-
for (const session of list) {
|
|
273
|
-
if (!session || typeof session !== 'object') continue;
|
|
274
|
-
if (!shouldEstimateUsageCostForSession(session)) {
|
|
275
|
-
skippedUnsupportedSessions += 1;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
supportedSessions += 1;
|
|
279
|
-
const cost = estimateUsageCostForSession(session, pricingIndex, currentProvider);
|
|
280
|
-
totalTokens += cost.totalSessionTokens;
|
|
281
|
-
if (!cost.pricing) {
|
|
282
|
-
missingPricingSessions += 1;
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (!cost.hasTokenBreakdown) {
|
|
286
|
-
missingTokenSessions += 1;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
totalCostUsd += cost.estimatedUsd;
|
|
290
|
-
estimatedSessions += 1;
|
|
291
|
-
estimatedTokens += cost.totalSessionTokens;
|
|
292
|
-
if (cost.pricing.source === 'public-catalog') catalogSessions += 1;
|
|
293
|
-
else configuredSessions += 1;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const coveragePercent = totalTokens > 0
|
|
297
|
-
? Math.round((estimatedTokens / totalTokens) * 100)
|
|
298
|
-
: (estimatedSessions > 0 ? 100 : 0);
|
|
299
|
-
return {
|
|
300
|
-
totalCostUsd,
|
|
301
|
-
estimatedSessions,
|
|
302
|
-
totalSessions: supportedSessions,
|
|
303
|
-
estimatedTokens,
|
|
304
|
-
totalTokens,
|
|
305
|
-
coveragePercent,
|
|
306
|
-
hasEstimate: estimatedSessions > 0,
|
|
307
|
-
configuredSessions,
|
|
308
|
-
catalogSessions,
|
|
309
|
-
missingPricingSessions,
|
|
310
|
-
missingTokenSessions,
|
|
311
|
-
skippedUnsupportedSessions
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function estimateUsageCostForSession(session, pricingIndex, currentProvider) {
|
|
316
|
-
const inputTokens = Number.isFinite(Number(session.inputTokens)) ? Math.max(0, Math.floor(Number(session.inputTokens))) : null;
|
|
317
|
-
const cachedInputTokens = Number.isFinite(Number(session.cachedInputTokens)) ? Math.max(0, Math.floor(Number(session.cachedInputTokens))) : 0;
|
|
318
|
-
const cacheCreationInputTokens = Number.isFinite(Number(session.cacheCreationInputTokens)) ? Math.max(0, Math.floor(Number(session.cacheCreationInputTokens))) : 0;
|
|
319
|
-
const outputTokens = Number.isFinite(Number(session.outputTokens)) ? Math.max(0, Math.floor(Number(session.outputTokens))) : null;
|
|
320
|
-
const reasoningOutputTokens = Number.isFinite(Number(session.reasoningOutputTokens)) ? Math.max(0, Math.floor(Number(session.reasoningOutputTokens))) : 0;
|
|
321
|
-
const billableInputTokens = Math.max(0, (inputTokens || 0) - cachedInputTokens - cacheCreationInputTokens);
|
|
322
|
-
const fallbackSessionTokens = billableInputTokens + cachedInputTokens + cacheCreationInputTokens + (outputTokens || 0) + reasoningOutputTokens;
|
|
323
|
-
const totalSessionTokens = Number.isFinite(Number(session.totalTokens))
|
|
324
|
-
? Math.max(0, Math.floor(Number(session.totalTokens)))
|
|
325
|
-
: fallbackSessionTokens;
|
|
326
|
-
const pricing = resolveUsagePricingForSession(session, pricingIndex, currentProvider);
|
|
327
|
-
const hasTokenBreakdown = !(inputTokens === null && outputTokens === null && reasoningOutputTokens === 0);
|
|
328
|
-
const reasoningRate = pricing
|
|
329
|
-
? ((pricing.reasoningOutput != null ? pricing.reasoningOutput : pricing.output) || 0)
|
|
330
|
-
: 0;
|
|
331
|
-
const visibleOutputTokens = Math.max(0, (outputTokens || 0) - reasoningOutputTokens);
|
|
332
|
-
const estimatedUsd = pricing && hasTokenBreakdown
|
|
333
|
-
? (
|
|
334
|
-
((pricing.input || 0) * billableInputTokens)
|
|
335
|
-
+ ((pricing.cacheRead || 0) * cachedInputTokens)
|
|
336
|
-
+ ((pricing.cacheWrite || 0) * cacheCreationInputTokens)
|
|
337
|
-
+ (reasoningRate * reasoningOutputTokens)
|
|
338
|
-
+ ((pricing.output || 0) * visibleOutputTokens)
|
|
339
|
-
) / 1000000
|
|
340
|
-
: 0;
|
|
341
|
-
return {
|
|
342
|
-
pricing,
|
|
343
|
-
hasTokenBreakdown,
|
|
344
|
-
totalSessionTokens,
|
|
345
|
-
estimatedUsd
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
115
|
export function createSessionComputed() {
|
|
350
116
|
return {
|
|
351
117
|
isSessionQueryEnabled() {
|
|
352
118
|
return isSessionQueryEnabled(this.sessionFilterSource);
|
|
353
119
|
},
|
|
120
|
+
sessionSourceOptions() {
|
|
121
|
+
return [
|
|
122
|
+
{ value: "all", label: this.t("common.all") },
|
|
123
|
+
{ value: "codex", label: this.t("sessions.source.codex") },
|
|
124
|
+
{ value: "claude", label: this.t("sessions.source.claudeCode") },
|
|
125
|
+
{ value: "gemini", label: this.t("sessions.source.gemini") },
|
|
126
|
+
{ value: "codebuddy", label: this.t("sessions.source.codebuddy") }
|
|
127
|
+
];
|
|
128
|
+
},
|
|
354
129
|
activeSessionExportKey() {
|
|
355
130
|
return this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
|
|
356
131
|
},
|
|
@@ -642,30 +417,8 @@ export function createSessionComputed() {
|
|
|
642
417
|
: this.sessionsUsageList;
|
|
643
418
|
const t = typeof this.t === 'function' ? this.t : null;
|
|
644
419
|
const usageRangeLabel = formatUsageRangeLabel(this.sessionsUsageTimeRange, t);
|
|
645
|
-
const estimatedCost = estimateUsageCostSummary(
|
|
646
|
-
filteredUsageSessions,
|
|
647
|
-
this.providersList,
|
|
648
|
-
this.currentProvider
|
|
649
|
-
);
|
|
650
420
|
const noneLabel = t ? t('common.none') : '暂无';
|
|
651
|
-
|
|
652
|
-
? (t ? t('usage.estimatedCost.note.excludesClaudePrefix') : '暂不含 Claude,')
|
|
653
|
-
: '';
|
|
654
|
-
const estimatedCostMethod = estimatedCost.catalogSessions > 0
|
|
655
|
-
? (estimatedCost.configuredSessions > 0
|
|
656
|
-
? (t ? t('usage.estimatedCost.method.configuredAndCatalog') : '按已配置单价 + 公开模型目录估算')
|
|
657
|
-
: (t ? t('usage.estimatedCost.method.catalog') : '按公开模型目录估算'))
|
|
658
|
-
: (t ? t('usage.estimatedCost.method.configured') : '按已配置单价估算');
|
|
659
|
-
const estimatedCostTitle = estimatedCost.hasEstimate
|
|
660
|
-
? (t ? t('usage.estimatedCost.detail.estimate', {
|
|
661
|
-
prefix: estimatedCostPrefix,
|
|
662
|
-
method: estimatedCostMethod,
|
|
663
|
-
estimate: formatUsageEstimatedCost(estimatedCost.totalCostUsd, { precise: true }),
|
|
664
|
-
covered: estimatedCost.estimatedSessions,
|
|
665
|
-
total: estimatedCost.totalSessions,
|
|
666
|
-
percent: estimatedCost.coveragePercent
|
|
667
|
-
}) : `${estimatedCostPrefix}${estimatedCostMethod},估算 ${formatUsageEstimatedCost(estimatedCost.totalCostUsd, { precise: true })},覆盖 ${estimatedCost.estimatedSessions}/${estimatedCost.totalSessions} 个会话,约 ${estimatedCost.coveragePercent}% token`)
|
|
668
|
-
: (t ? t('usage.estimatedCost.detail.missing', { prefix: estimatedCostPrefix }) : `${estimatedCostPrefix}缺少可匹配的模型单价或 token 拆分。请先补 models.cost,或确认会话已记录 input/output token。`);
|
|
421
|
+
|
|
669
422
|
return [
|
|
670
423
|
{ key: 'sessions', label: t ? t('usage.summary.sessions') : '总会话数', value: formatUsageSummaryNumber(summary.totalSessions || 0) },
|
|
671
424
|
{ key: 'messages', label: t ? t('usage.summary.messages') : '总消息数', value: formatUsageSummaryNumber(summary.totalMessages || 0) },
|
|
@@ -681,12 +434,6 @@ export function createSessionComputed() {
|
|
|
681
434
|
value: formatCompactUsageSummaryNumber(summary.totalContextWindow || 0),
|
|
682
435
|
title: formatUsageSummaryNumber(summary.totalContextWindow || 0)
|
|
683
436
|
},
|
|
684
|
-
{
|
|
685
|
-
key: 'estimated-cost',
|
|
686
|
-
label: t ? t('usage.summary.estimatedCost', { range: usageRangeLabel }) : `预估费用 · ${usageRangeLabel}`,
|
|
687
|
-
value: estimatedCost.hasEstimate ? formatUsageEstimatedCost(estimatedCost.totalCostUsd) : '0',
|
|
688
|
-
title: estimatedCostTitle
|
|
689
|
-
},
|
|
690
437
|
{
|
|
691
438
|
key: 'active-duration',
|
|
692
439
|
label: t ? t('usage.summary.activeDuration') : '活跃时长',
|
|
@@ -747,7 +494,6 @@ export function createSessionComputed() {
|
|
|
747
494
|
const baseBuckets = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.buckets)
|
|
748
495
|
? this.sessionUsageCharts.buckets
|
|
749
496
|
: [];
|
|
750
|
-
const pricingIndex = buildUsagePricingIndex(this.providersList);
|
|
751
497
|
const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
|
|
752
498
|
const sessions = compareEnabled
|
|
753
499
|
? this.sessionsUsageList
|
|
@@ -766,9 +512,7 @@ export function createSessionComputed() {
|
|
|
766
512
|
sessionCount: 0,
|
|
767
513
|
messageCount: 0,
|
|
768
514
|
tokenTotal: 0,
|
|
769
|
-
|
|
770
|
-
estimatedSessions: 0,
|
|
771
|
-
hasCostEstimate: false
|
|
515
|
+
|
|
772
516
|
});
|
|
773
517
|
if (compareEnabled) {
|
|
774
518
|
const baseMs = Date.parse(`${bucket.key}T00:00:00.000Z`);
|
|
@@ -781,9 +525,7 @@ export function createSessionComputed() {
|
|
|
781
525
|
sessionCount: 0,
|
|
782
526
|
messageCount: 0,
|
|
783
527
|
tokenTotal: 0,
|
|
784
|
-
|
|
785
|
-
estimatedSessions: 0,
|
|
786
|
-
hasCostEstimate: false
|
|
528
|
+
|
|
787
529
|
});
|
|
788
530
|
}
|
|
789
531
|
}
|
|
@@ -808,15 +550,7 @@ export function createSessionComputed() {
|
|
|
808
550
|
row.messageCount += messageCount;
|
|
809
551
|
row.tokenTotal += tokenTotal;
|
|
810
552
|
|
|
811
|
-
|
|
812
|
-
const cost = estimateUsageCostForSession(session, pricingIndex, this.currentProvider);
|
|
813
|
-
if (cost.pricing && cost.hasTokenBreakdown) {
|
|
814
|
-
row.estimatedCostUsd += cost.estimatedUsd;
|
|
815
|
-
row.estimatedSessions += 1;
|
|
816
|
-
row.hasCostEstimate = true;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
553
|
+
}
|
|
820
554
|
|
|
821
555
|
const currentKeys = baseBuckets.map((bucket) => bucket && bucket.key).filter(Boolean);
|
|
822
556
|
const rows = currentKeys
|
|
@@ -825,7 +559,7 @@ export function createSessionComputed() {
|
|
|
825
559
|
.sort((a, b) => b.key.localeCompare(a.key, 'en-US'));
|
|
826
560
|
const rowsWithCompare = rows.map((row) => {
|
|
827
561
|
if (!compareEnabled) {
|
|
828
|
-
return { ...row, compareEnabled: false, prevKey: '', prevTokenTotal: 0
|
|
562
|
+
return { ...row, compareEnabled: false, prevKey: '', prevTokenTotal: 0 };
|
|
829
563
|
}
|
|
830
564
|
const baseMs = Date.parse(`${row.key}T00:00:00.000Z`);
|
|
831
565
|
const prevKey = Number.isFinite(baseMs)
|
|
@@ -837,11 +571,9 @@ export function createSessionComputed() {
|
|
|
837
571
|
compareEnabled: true,
|
|
838
572
|
prevKey,
|
|
839
573
|
prevTokenTotal: prevRow ? prevRow.tokenTotal : 0,
|
|
840
|
-
prevCostUsd: prevRow ? prevRow.estimatedCostUsd : 0
|
|
841
574
|
};
|
|
842
575
|
});
|
|
843
576
|
const maxTokens = rowsWithCompare.reduce((max, item) => Math.max(max, item.tokenTotal, item.prevTokenTotal || 0), 0);
|
|
844
|
-
const maxCost = rowsWithCompare.reduce((max, item) => Math.max(max, item.estimatedCostUsd, item.prevCostUsd || 0), 0);
|
|
845
577
|
|
|
846
578
|
return {
|
|
847
579
|
rows: rowsWithCompare.map((row) => ({
|
|
@@ -851,14 +583,9 @@ export function createSessionComputed() {
|
|
|
851
583
|
tokenPercent: maxTokens > 0 ? Math.round((row.tokenTotal / maxTokens) * 1000) / 10 : 0,
|
|
852
584
|
prevTokenPercent: row.compareEnabled && maxTokens > 0 ? Math.round(((row.prevTokenTotal || 0) / maxTokens) * 1000) / 10 : 0,
|
|
853
585
|
prevTokenTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',
|
|
854
|
-
|
|
855
|
-
costTitle: row.hasCostEstimate ? formatUsageEstimatedCost(row.estimatedCostUsd, { precise: true }) : '0',
|
|
856
|
-
costPercent: maxCost > 0 ? Math.round((row.estimatedCostUsd / maxCost) * 1000) / 10 : 0,
|
|
857
|
-
prevCostPercent: row.compareEnabled && maxCost > 0 ? Math.round(((row.prevCostUsd || 0) / maxCost) * 1000) / 10 : 0,
|
|
858
|
-
prevCostTitle: row.compareEnabled ? formatUsageEstimatedCost(row.prevCostUsd || 0, { precise: true }) : ''
|
|
586
|
+
prevCostTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',
|
|
859
587
|
})),
|
|
860
588
|
maxTokens,
|
|
861
|
-
maxCost
|
|
862
589
|
};
|
|
863
590
|
},
|
|
864
591
|
|
|
@@ -875,7 +602,6 @@ export function createSessionComputed() {
|
|
|
875
602
|
const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
|
|
876
603
|
? this.sessionUsageCharts.filteredSessions
|
|
877
604
|
: this.sessionsUsageList;
|
|
878
|
-
const pricingIndex = buildUsagePricingIndex(this.providersList);
|
|
879
605
|
const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
|
|
880
606
|
const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7;
|
|
881
607
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
@@ -886,11 +612,7 @@ export function createSessionComputed() {
|
|
|
886
612
|
let sessionCount = 0;
|
|
887
613
|
let messageCount = 0;
|
|
888
614
|
let tokenTotal = 0;
|
|
889
|
-
let estimatedCostUsd = 0;
|
|
890
|
-
let hasCostEstimate = false;
|
|
891
615
|
let prevTokenTotal = 0;
|
|
892
|
-
let prevEstimatedCostUsd = 0;
|
|
893
|
-
let prevHasCostEstimate = false;
|
|
894
616
|
const modelMap = new Map();
|
|
895
617
|
const sessionRows = [];
|
|
896
618
|
for (const session of (Array.isArray(sessions) ? sessions : [])) {
|
|
@@ -914,19 +636,7 @@ export function createSessionComputed() {
|
|
|
914
636
|
} else if (isPrev) {
|
|
915
637
|
prevTokenTotal += sessionTokens;
|
|
916
638
|
}
|
|
917
|
-
|
|
918
|
-
const cost = estimateUsageCostForSession(session, pricingIndex, this.currentProvider);
|
|
919
|
-
if (cost.pricing && cost.hasTokenBreakdown) {
|
|
920
|
-
if (isCurrent) {
|
|
921
|
-
estimatedCostUsd += cost.estimatedUsd;
|
|
922
|
-
hasCostEstimate = true;
|
|
923
|
-
} else if (isPrev) {
|
|
924
|
-
prevEstimatedCostUsd += cost.estimatedUsd;
|
|
925
|
-
prevHasCostEstimate = true;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
const model = typeof session.model === 'string' ? session.model.trim() : '';
|
|
639
|
+
const model = typeof session.model === 'string' ? session.model.trim() : '';
|
|
930
640
|
if (isCurrent && model) {
|
|
931
641
|
modelMap.set(model, (modelMap.get(model) || 0) + 1);
|
|
932
642
|
}
|
|
@@ -963,12 +673,9 @@ export function createSessionComputed() {
|
|
|
963
673
|
messageCount,
|
|
964
674
|
tokenTotal,
|
|
965
675
|
tokenLabel: formatUsageSummaryNumber(tokenTotal),
|
|
966
|
-
costLabel: hasCostEstimate ? formatUsageEstimatedCost(estimatedCostUsd) : '0',
|
|
967
676
|
prevTokenTotal,
|
|
968
677
|
prevTokenLabel: compareEnabled ? formatUsageSummaryNumber(prevTokenTotal) : '0',
|
|
969
678
|
deltaTokenLabel: compareEnabled ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : '0',
|
|
970
|
-
prevCostLabel: compareEnabled ? (prevHasCostEstimate ? formatUsageEstimatedCost(prevEstimatedCostUsd) : '0') : '0',
|
|
971
|
-
deltaCostLabel: compareEnabled ? formatSignedUsageEstimatedCost(estimatedCostUsd - prevEstimatedCostUsd, { precise: true }) : '0',
|
|
972
679
|
topSessions,
|
|
973
680
|
topModels
|
|
974
681
|
};
|
|
@@ -994,6 +701,35 @@ export function createSessionComputed() {
|
|
|
994
701
|
return Math.max(0, Math.floor(totalCount));
|
|
995
702
|
}
|
|
996
703
|
return Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
sessionContextUtilization() {
|
|
707
|
+
const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
|
|
708
|
+
const utilizationMap = {};
|
|
709
|
+
for (const session of list) {
|
|
710
|
+
if (!session || typeof session !== 'object') continue;
|
|
711
|
+
const key = this.getSessionExportKey(session);
|
|
712
|
+
if (!key) continue;
|
|
713
|
+
const totalTokens = Number.isFinite(Number(session.totalTokens))
|
|
714
|
+
? Math.max(0, Math.floor(Number(session.totalTokens)))
|
|
715
|
+
: 0;
|
|
716
|
+
const contextWindow = Number.isFinite(Number(session.contextWindow))
|
|
717
|
+
? Math.max(0, Math.floor(Number(session.contextWindow)))
|
|
718
|
+
: 0;
|
|
719
|
+
if (contextWindow <= 0) {
|
|
720
|
+
utilizationMap[key] = { percent: 0, level: 'normal' };
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const percent = Math.min(100, Math.round((totalTokens / contextWindow) * 100));
|
|
724
|
+
let level = 'normal';
|
|
725
|
+
if (percent >= 95) {
|
|
726
|
+
level = 'critical';
|
|
727
|
+
} else if (percent >= 80) {
|
|
728
|
+
level = 'warning';
|
|
729
|
+
}
|
|
730
|
+
utilizationMap[key] = { percent, level };
|
|
731
|
+
}
|
|
732
|
+
return utilizationMap;
|
|
997
733
|
}
|
|
998
734
|
};
|
|
999
735
|
}
|
|
@@ -420,6 +420,25 @@ export function createAgentsMethods(options = {}) {
|
|
|
420
420
|
this.resetAgentsDiffState();
|
|
421
421
|
}
|
|
422
422
|
},
|
|
423
|
+
async pasteAgentsContent() {
|
|
424
|
+
if (this.agentsLoading || this.agentsSaving || this.agentsDiffVisible) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
let text = '';
|
|
428
|
+
try {
|
|
429
|
+
text = await navigator.clipboard.readText();
|
|
430
|
+
} catch (_) {
|
|
431
|
+
this.showMessage('无法读取剪贴板', 'error');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (typeof text !== 'string' || !text) {
|
|
435
|
+
this.showMessage('剪贴板为空', 'info');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.agentsContent = text;
|
|
439
|
+
this.onAgentsContentInput();
|
|
440
|
+
this.showMessage('已粘贴', 'success');
|
|
441
|
+
},
|
|
423
442
|
buildAgentsDiffFingerprint() {
|
|
424
443
|
const context = this.agentsContext || 'codex';
|
|
425
444
|
const fileName = context === 'openclaw-workspace'
|
|
@@ -10,8 +10,7 @@ export function createClaudeConfigMethods(options = {}) {
|
|
|
10
10
|
|
|
11
11
|
onClaudeModelChange() {
|
|
12
12
|
const name = this.currentClaudeConfig;
|
|
13
|
-
if (!name) {
|
|
14
|
-
this.showMessage('请先选择配置', 'error');
|
|
13
|
+
if (!name || name === 'claude-local') {
|
|
15
14
|
return;
|
|
16
15
|
}
|
|
17
16
|
const model = (this.currentClaudeModel || '').trim();
|
|
@@ -41,6 +40,11 @@ export function createClaudeConfigMethods(options = {}) {
|
|
|
41
40
|
if (this.currentClaudeConfig) {
|
|
42
41
|
try { localStorage.setItem('currentClaudeConfig', this.currentClaudeConfig); } catch (_) {}
|
|
43
42
|
}
|
|
43
|
+
this.syncClaudeBridgeProviders();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async syncClaudeBridgeProviders() {
|
|
47
|
+
try { await api('claude-local-bridge-sync-providers', { providers: this.claudeConfigs || {} }); } catch (_) {}
|
|
44
48
|
},
|
|
45
49
|
|
|
46
50
|
openCloneClaudeConfigModal(name, config) {
|
|
@@ -61,6 +65,7 @@ export function createClaudeConfigMethods(options = {}) {
|
|
|
61
65
|
baseUrl: config.baseUrl || '',
|
|
62
66
|
model: config.model || ''
|
|
63
67
|
};
|
|
68
|
+
this.showEditClaudeConfigKey = false;
|
|
64
69
|
this.showEditConfigModal = true;
|
|
65
70
|
},
|
|
66
71
|
|
|
@@ -77,9 +82,14 @@ export function createClaudeConfigMethods(options = {}) {
|
|
|
77
82
|
|
|
78
83
|
closeEditConfigModal() {
|
|
79
84
|
this.showEditConfigModal = false;
|
|
85
|
+
this.showEditClaudeConfigKey = false;
|
|
80
86
|
this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
|
|
81
87
|
},
|
|
82
88
|
|
|
89
|
+
toggleEditClaudeConfigKey() {
|
|
90
|
+
this.showEditClaudeConfigKey = !this.showEditClaudeConfigKey;
|
|
91
|
+
},
|
|
92
|
+
|
|
83
93
|
async saveAndApplyConfig() {
|
|
84
94
|
const name = this.editingConfig.name;
|
|
85
95
|
this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
|
|
@@ -195,6 +205,65 @@ export function createClaudeConfigMethods(options = {}) {
|
|
|
195
205
|
baseUrl: '',
|
|
196
206
|
model: ''
|
|
197
207
|
};
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async loadClaudeLocalBridgeStatus() {
|
|
211
|
+
try {
|
|
212
|
+
const res = await api('claude-local-bridge-status');
|
|
213
|
+
if (res && !res.error) {
|
|
214
|
+
if (Array.isArray(res.excludedProviders)) {
|
|
215
|
+
this.claudeLocalBridgeExcluded = res.excludedProviders;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (e) { /* ignore */ }
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async toggleClaudeLocalBridge(enable) {
|
|
222
|
+
try {
|
|
223
|
+
const res = await api('claude-local-bridge-toggle', { enable });
|
|
224
|
+
if (res.error) {
|
|
225
|
+
this.showMessage(res.error, 'error');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (enable) {
|
|
229
|
+
this.showMessage('Claude 本地负载均衡已启用', 'success');
|
|
230
|
+
} else {
|
|
231
|
+
this.showMessage('Claude 本地负载均衡已关闭', 'success');
|
|
232
|
+
}
|
|
233
|
+
} catch (e) {
|
|
234
|
+
this.showMessage('操作失败', 'error');
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
async toggleClaudeLocalBridgeExcluded(providerName) {
|
|
239
|
+
const name = String(providerName || '').trim();
|
|
240
|
+
if (!name) return;
|
|
241
|
+
const idx = this.claudeLocalBridgeExcluded.indexOf(name);
|
|
242
|
+
const next = [...this.claudeLocalBridgeExcluded];
|
|
243
|
+
if (idx >= 0) {
|
|
244
|
+
next.splice(idx, 1);
|
|
245
|
+
} else {
|
|
246
|
+
next.push(name);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const res = await api('claude-local-bridge-set-excluded', { names: next });
|
|
250
|
+
if (res && !res.error) {
|
|
251
|
+
this.claudeLocalBridgeExcluded = next;
|
|
252
|
+
}
|
|
253
|
+
} catch (e) { /* ignore */ }
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
isClaudeLocalBridgeExcluded(providerName) {
|
|
257
|
+
return this.claudeLocalBridgeExcluded.indexOf(String(providerName || '').trim()) >= 0;
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
claudeLocalBridgeCandidateProviders() {
|
|
261
|
+
return Object.keys(this.claudeConfigs || {}).filter(name => name && !this.isClaudeLocalBridgeExcluded(name))
|
|
262
|
+
.map(name => ({ name, ...this.claudeConfigs[name] }));
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
claudeLocalBridgeConfigured() {
|
|
266
|
+
return this.claudeLocalBridgeCandidateProviders().some(p => p.hasKey);
|
|
198
267
|
}
|
|
199
268
|
};
|
|
200
269
|
}
|
|
@@ -696,6 +696,26 @@ export function createCodexConfigMethods(options = {}) {
|
|
|
696
696
|
}
|
|
697
697
|
},
|
|
698
698
|
|
|
699
|
+
async pasteConfigTemplateContent() {
|
|
700
|
+
if (this.configTemplateApplying || this.configTemplateDiffLoading || this.configTemplateDiffVisible) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
let text = '';
|
|
704
|
+
try {
|
|
705
|
+
text = await navigator.clipboard.readText();
|
|
706
|
+
} catch (_) {
|
|
707
|
+
this.showMessage('无法读取剪贴板', 'error');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (typeof text !== 'string' || !text) {
|
|
711
|
+
this.showMessage('剪贴板为空', 'info');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this.configTemplateContent = text;
|
|
715
|
+
this.onConfigTemplateContentInput();
|
|
716
|
+
this.showMessage('已粘贴', 'success');
|
|
717
|
+
},
|
|
718
|
+
|
|
699
719
|
buildConfigTemplateDiffFingerprint() {
|
|
700
720
|
const content = typeof this.configTemplateContent === 'string' ? this.configTemplateContent : '';
|
|
701
721
|
return `${content.length}::${content}`;
|