feat: add models management and usage history components

- Introduced RequestContentDialog for displaying request content details.
- Added UsageBarChart for visualizing token usage data.
- Implemented UsageHistorySection to manage and display usage history with filtering and pagination.
- Created provider-types for managing provider-related types.
- Developed ModelsPage to encapsulate models configuration, providers, and usage history.
- Defined usage-history types and utility functions for managing usage data.
- Updated routing to include models page and redirect agents to models.
- Refactored chat store to integrate models instead of agents.
- Established models store for managing model-related state and data fetching.
This commit is contained in:
duanshuwen
2026-04-18 09:41:59 +08:00
parent 1205a96661
commit 85d92b937f
28 changed files with 343 additions and 258 deletions

View File

@@ -1,6 +1,9 @@
import { sessionStore } from '@electron/gateway/session-store';
import { getTranscriptFilePath } from '@electron/utils/token-usage-writer';
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/agents';
import {
getTranscriptFilePath,
getTranscriptPathCandidates,
} from '@electron/utils/token-usage-writer';
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/models';
import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils';
import { fail, ok, parseJsonBody } from '../route-utils';
@@ -41,20 +44,45 @@ export async function handleSessionRoutes(
try {
const fsP = await import('node:fs/promises');
const transcriptPath = getTranscriptFilePath(identity.sessionKey);
let raw: string;
let transcriptPath = getTranscriptFilePath(identity.sessionKey);
let raw: string | null = null;
let lastError: unknown = null;
try {
raw = await fsP.readFile(transcriptPath, 'utf8');
} catch (error: any) {
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
if (error?.code === 'ENOENT' && requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
raw = await fsP.readFile(getTranscriptFilePath(requestedSessionKey), 'utf8');
} else {
throw error;
for (const candidatePath of getTranscriptPathCandidates(identity.sessionKey)) {
try {
raw = await fsP.readFile(candidatePath, 'utf8');
transcriptPath = candidatePath;
break;
} catch (error) {
lastError = error;
if ((error as { code?: string } | null)?.code !== 'ENOENT') {
throw error;
}
}
}
if (raw === null) {
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
if (requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
for (const candidatePath of getTranscriptPathCandidates(requestedSessionKey)) {
try {
raw = await fsP.readFile(candidatePath, 'utf8');
transcriptPath = candidatePath;
break;
} catch (candidateError) {
lastError = candidateError;
if ((candidateError as { code?: string } | null)?.code !== 'ENOENT') {
throw candidateError;
}
}
}
}
}
if (raw === null) {
throw lastError ?? Object.assign(new Error('Transcript not found'), { code: 'ENOENT' });
}
const lines = raw.split(/\r?\n/).filter(Boolean);
const messages = lines.flatMap((line) => {
try {
@@ -90,19 +118,18 @@ export async function handleSessionRoutes(
sessionStore.deleteSession(sessionKey);
const transcriptPath = getTranscriptFilePath(sessionKey);
try {
const fsP = await import('node:fs/promises');
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
} catch {
if (rawSessionKey && rawSessionKey !== sessionKey) {
try {
const fsP = await import('node:fs/promises');
const legacyTranscriptPath = getTranscriptFilePath(rawSessionKey);
await fsP.rename(legacyTranscriptPath, legacyTranscriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
} catch {
// Best effort: transcript may not exist yet.
}
const fsP = await import('node:fs/promises');
const transcriptPathCandidates = Array.from(new Set([
...getTranscriptPathCandidates(sessionKey),
...(rawSessionKey && rawSessionKey !== sessionKey ? getTranscriptPathCandidates(rawSessionKey) : []),
]));
for (const transcriptPath of transcriptPathCandidates) {
try {
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
break;
} catch {
continue;
}
}