mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-02 02:38:38 +00:00
refactor: move fetching messages to module
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
|
||||
@@ -103,13 +102,10 @@ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
|
||||
}
|
||||
|
||||
async function getSessionMessages(
|
||||
projectName: string,
|
||||
sessionId: string,
|
||||
limit: number | null,
|
||||
offset: number,
|
||||
): Promise<ClaudeHistoryMessagesResult> {
|
||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||
|
||||
try {
|
||||
const jsonLPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
|
||||
|
||||
@@ -117,6 +113,7 @@ async function getSessionMessages(
|
||||
return { messages: [], total: 0, hasMore: false };
|
||||
}
|
||||
|
||||
const projectDir = path.dirname(jsonLPath);
|
||||
const files = await fsp.readdir(projectDir);
|
||||
const agentFiles = files.filter((file) => file.endsWith('.jsonl') && file.startsWith('agent-'));
|
||||
|
||||
@@ -413,14 +410,11 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
||||
sessionId: string,
|
||||
options: FetchHistoryOptions = {},
|
||||
): Promise<FetchHistoryResult> {
|
||||
const { projectName, limit = null, offset = 0 } = options;
|
||||
if (!projectName) {
|
||||
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
||||
}
|
||||
const { limit = null, offset = 0 } = options;
|
||||
|
||||
let result: ClaudeHistoryResult;
|
||||
try {
|
||||
result = await getSessionMessages(projectName, sessionId, limit, offset);
|
||||
result = await getSessionMessages(sessionId, limit, offset);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
|
||||
|
||||
@@ -49,6 +49,29 @@ const readOptionalQueryString = (value: unknown): string | undefined => {
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
};
|
||||
|
||||
const parseOptionalBooleanQuery = (value: unknown, name: string): boolean | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = readOptionalQueryString(value);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (normalized === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (normalized === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new AppError(`${name} must be "true" or "false".`, {
|
||||
code: 'INVALID_QUERY_PARAMETER',
|
||||
statusCode: 400,
|
||||
});
|
||||
};
|
||||
|
||||
const parseMcpScope = (value: unknown): McpScope | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
@@ -260,7 +283,8 @@ router.delete(
|
||||
'/sessions/:sessionId',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const sessionId = parseSessionId(req.params.sessionId);
|
||||
const result = await sessionsService.deleteSessionById(sessionId);
|
||||
const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? false;
|
||||
const result = await sessionsService.deleteSessionById(sessionId, deletedFromDisk);
|
||||
res.json(createApiSuccessResponse(result));
|
||||
}),
|
||||
);
|
||||
@@ -275,4 +299,36 @@ router.put(
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/sessions/:sessionId/messages',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const sessionId = parseSessionId(req.params.sessionId);
|
||||
const limitRaw = readOptionalQueryString(req.query.limit);
|
||||
const offsetRaw = readOptionalQueryString(req.query.offset);
|
||||
|
||||
const limit = limitRaw === undefined ? null : Number.parseInt(limitRaw, 10);
|
||||
const offset = offsetRaw === undefined ? 0 : Number.parseInt(offsetRaw, 10);
|
||||
|
||||
if (limitRaw !== undefined && Number.isNaN(limit)) {
|
||||
throw new AppError('limit must be a valid integer.', {
|
||||
code: 'INVALID_QUERY_PARAMETER',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (offsetRaw !== undefined && Number.isNaN(offset)) {
|
||||
throw new AppError('offset must be a valid integer.', {
|
||||
code: 'INVALID_QUERY_PARAMETER',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await sessionsService.fetchHistory(sessionId, {
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
} from '@/shared/types.js';
|
||||
import { AppError } from '@/shared/utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* Removes one file if it exists.
|
||||
*/
|
||||
@@ -54,20 +53,54 @@ export const sessionsService = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches normalized persisted session history for one provider/session pair.
|
||||
* Fetches persisted history by session id.
|
||||
*
|
||||
* Provider and provider-specific lookup hints are resolved from the indexed
|
||||
* session metadata in the database.
|
||||
*/
|
||||
fetchHistory(
|
||||
providerName: string,
|
||||
sessionId: string,
|
||||
options?: FetchHistoryOptions,
|
||||
options: Pick<FetchHistoryOptions, 'limit' | 'offset'> = {},
|
||||
): Promise<FetchHistoryResult> {
|
||||
return providerRegistry.resolveProvider(providerName).sessions.fetchHistory(sessionId, options);
|
||||
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.
|
||||
*/
|
||||
deleteSessionById(sessionId: string): { sessionId: string } {
|
||||
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.`, {
|
||||
@@ -76,7 +109,7 @@ export const sessionsService = {
|
||||
});
|
||||
}
|
||||
|
||||
return { sessionId };
|
||||
return { sessionId, deletedFromDisk: removedFromDisk };
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user