mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-08 14:45:50 +08:00
refactor: move projects provider specific logic into respective session providers
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import { getSessionMessages } from '@/projects.js';
|
||||
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';
|
||||
|
||||
import type { IProviderSessions } from '@/shared/interfaces.js';
|
||||
import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
||||
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
||||
import { sessionsDb } from '@/modules/database/index.js';
|
||||
|
||||
const PROVIDER = 'claude';
|
||||
|
||||
@@ -15,17 +21,186 @@ type ClaudeToolResult = {
|
||||
type ClaudeHistoryResult =
|
||||
| AnyRecord[]
|
||||
| {
|
||||
messages?: AnyRecord[];
|
||||
total?: number;
|
||||
hasMore?: boolean;
|
||||
};
|
||||
messages?: AnyRecord[];
|
||||
total?: number;
|
||||
hasMore?: boolean;
|
||||
};
|
||||
|
||||
const loadClaudeSessionMessages = getSessionMessages as unknown as (
|
||||
type ClaudeHistoryMessagesResult =
|
||||
| AnyRecord[]
|
||||
| {
|
||||
messages: AnyRecord[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
offset?: number;
|
||||
limit?: number | null;
|
||||
};
|
||||
|
||||
async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
|
||||
const tools: AnyRecord[] = [];
|
||||
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = JSON.parse(line) as AnyRecord;
|
||||
|
||||
if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
|
||||
for (const part of entry.message.content as AnyRecord[]) {
|
||||
if (part.type === 'tool_use') {
|
||||
tools.push({
|
||||
toolId: part.id,
|
||||
toolName: part.name,
|
||||
toolInput: part.input,
|
||||
timestamp: entry.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
|
||||
for (const part of entry.message.content as AnyRecord[]) {
|
||||
if (part.type !== 'tool_result') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tool = tools.find((candidate) => candidate.toolId === part.tool_use_id);
|
||||
if (!tool) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tool.toolResult = {
|
||||
content: typeof part.content === 'string'
|
||||
? part.content
|
||||
: Array.isArray(part.content)
|
||||
? part.content
|
||||
.map((contentPart: AnyRecord) => contentPart?.text || '')
|
||||
.join('\n')
|
||||
: JSON.stringify(part.content),
|
||||
isError: Boolean(part.is_error),
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed lines that can happen during concurrent writes.
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Error parsing agent file ${filePath}:`, message);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
async function getSessionMessages(
|
||||
projectName: string,
|
||||
sessionId: string,
|
||||
limit: number | null,
|
||||
offset: number,
|
||||
) => Promise<ClaudeHistoryResult>;
|
||||
): Promise<ClaudeHistoryMessagesResult> {
|
||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||
|
||||
try {
|
||||
const jsonLPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
|
||||
|
||||
if (!jsonLPath) {
|
||||
return { messages: [], total: 0, hasMore: false };
|
||||
}
|
||||
|
||||
const files = await fsp.readdir(projectDir);
|
||||
const agentFiles = files.filter((file) => file.endsWith('.jsonl') && file.startsWith('agent-'));
|
||||
|
||||
const messages: AnyRecord[] = [];
|
||||
const agentToolsCache = new Map<string, AnyRecord[]>();
|
||||
|
||||
const fileStream = fs.createReadStream(jsonLPath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = JSON.parse(line) as AnyRecord;
|
||||
if (entry.sessionId === sessionId) {
|
||||
messages.push(entry);
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed JSONL lines that can happen during concurrent writes.
|
||||
}
|
||||
}
|
||||
|
||||
const agentIds = new Set<string>();
|
||||
for (const message of messages) {
|
||||
const agentId = message.toolUseResult?.agentId;
|
||||
if (agentId) {
|
||||
agentIds.add(String(agentId));
|
||||
}
|
||||
}
|
||||
|
||||
for (const agentId of agentIds) {
|
||||
const agentFileName = `agent-${agentId}.jsonl`;
|
||||
if (!agentFiles.includes(agentFileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const agentFilePath = path.join(projectDir, agentFileName);
|
||||
const tools = await parseAgentTools(agentFilePath);
|
||||
agentToolsCache.set(agentId, tools);
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
const agentId = message.toolUseResult?.agentId;
|
||||
if (!agentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const agentTools = agentToolsCache.get(String(agentId));
|
||||
if (agentTools && agentTools.length > 0) {
|
||||
message.subagentTools = agentTools;
|
||||
}
|
||||
}
|
||||
|
||||
const sortedMessages = messages.sort(
|
||||
(a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
|
||||
);
|
||||
const total = sortedMessages.length;
|
||||
|
||||
if (limit === null) {
|
||||
return sortedMessages;
|
||||
}
|
||||
|
||||
const startIndex = Math.max(0, total - offset - limit);
|
||||
const endIndex = total - offset;
|
||||
const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
|
||||
const hasMore = startIndex > 0;
|
||||
|
||||
return {
|
||||
messages: paginatedMessages,
|
||||
total,
|
||||
hasMore,
|
||||
offset,
|
||||
limit,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error reading messages for session ${sessionId}:`, error);
|
||||
return limit === null ? [] : { messages: [], total: 0, hasMore: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude writes internal command and system reminder entries into history.
|
||||
@@ -245,7 +420,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
|
||||
|
||||
let result: ClaudeHistoryResult;
|
||||
try {
|
||||
result = await loadClaudeSessionMessages(projectName, sessionId, limit, offset);
|
||||
result = await getSessionMessages(projectName, sessionId, limit, offset);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
|
||||
|
||||
Reference in New Issue
Block a user