Files
claudecodeui/server/modules/providers/services/sessions.service.ts
2026-04-27 14:30:09 +03:00

131 lines
3.8 KiB
TypeScript

import fsp from 'node:fs/promises';
import { sessionsDb } from '@/modules/database/index.js';
import { providerRegistry } from '@/modules/providers/provider.registry.js';
import type {
FetchHistoryOptions,
FetchHistoryResult,
LLMProvider,
NormalizedMessage,
} from '@/shared/types.js';
import { AppError } from '@/shared/utils.js';
/**
* Removes one file if it exists.
*/
async function removeFileIfExists(filePath: string): Promise<boolean> {
try {
await fsp.unlink(filePath);
return true;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
return false;
}
throw error;
}
}
/**
* Application service for provider-backed session message operations.
*
* Callers pass a provider id and this service resolves the concrete provider
* class, keeping normalization/history call sites decoupled from implementation
* file layout.
*/
export const sessionsService = {
/**
* Lists provider ids that can load session history and normalize live messages.
*/
listProviderIds(): LLMProvider[] {
return providerRegistry.listProviders().map((provider) => provider.id);
},
/**
* Normalizes one provider-native event into frontend session message events.
*/
normalizeMessage(
providerName: string,
raw: unknown,
sessionId: string | null,
): NormalizedMessage[] {
return providerRegistry.resolveProvider(providerName).sessions.normalizeMessage(raw, sessionId);
},
/**
* Fetches persisted history by session id.
*
* Provider and provider-specific lookup hints are resolved from the indexed
* session metadata in the database.
*/
fetchHistory(
sessionId: string,
options: Pick<FetchHistoryOptions, 'limit' | 'offset'> = {},
): Promise<FetchHistoryResult> {
const session = sessionsDb.getSessionById(sessionId);
if (!session) {
throw new AppError(`Session "${sessionId}" was not found.`, {
code: 'SESSION_NOT_FOUND',
statusCode: 404,
});
}
const provider = session.provider as LLMProvider;
return providerRegistry.resolveProvider(provider).sessions.fetchHistory(sessionId, {
limit: options.limit ?? null,
offset: options.offset ?? 0,
projectPath: session.project_path ?? '',
});
},
/**
* Deletes one persisted session row by id.
*
* When `deletedFromDisk` is true and a session `jsonl_path` exists, the path
* is deleted from disk before the DB row is removed.
*/
async deleteSessionById(
sessionId: string,
deletedFromDisk = false,
): Promise<{ sessionId: string; deletedFromDisk: boolean }> {
const session = sessionsDb.getSessionById(sessionId);
if (!session) {
throw new AppError(`Session "${sessionId}" was not found.`, {
code: 'SESSION_NOT_FOUND',
statusCode: 404,
});
}
let removedFromDisk = false;
if (deletedFromDisk && session.jsonl_path) {
removedFromDisk = await removeFileIfExists(session.jsonl_path);
}
const deleted = sessionsDb.deleteSessionById(sessionId);
if (!deleted) {
throw new AppError(`Session "${sessionId}" was not found.`, {
code: 'SESSION_NOT_FOUND',
statusCode: 404,
});
}
return { sessionId, deletedFromDisk: removedFromDisk };
},
/**
* Renames one session by id without requiring the caller to pass provider.
*/
renameSessionById(sessionId: string, summary: string): { sessionId: string; summary: string } {
const session = sessionsDb.getSessionById(sessionId);
if (!session) {
throw new AppError(`Session "${sessionId}" was not found.`, {
code: 'SESSION_NOT_FOUND',
statusCode: 404,
});
}
sessionsDb.updateSessionCustomName(sessionId, summary);
return { sessionId, summary };
},
};