2025-10-04 12:10:22 +00:00
|
|
|
import { Path } from "Enums/Path";
|
2025-09-30 20:20:24 +00:00
|
|
|
import { Resolve } from "./DependencyService";
|
|
|
|
|
import { FileSystemService } from "./FileSystemService";
|
|
|
|
|
import { Services } from "./Services";
|
2025-10-04 12:10:22 +00:00
|
|
|
import { Conversation } from "Conversations/Conversation";
|
2025-10-05 12:06:05 +00:00
|
|
|
import { ConversationContent } from "Conversations/ConversationContent";
|
2025-12-19 12:30:51 +00:00
|
|
|
import { Attachment } from "Conversations/Attachment";
|
2025-11-17 19:02:15 +00:00
|
|
|
import { Exception } from "Helpers/Exception";
|
2025-12-19 12:30:51 +00:00
|
|
|
import type { IAIFileService } from "AIClasses/IAIFileService";
|
2025-12-23 12:04:29 +00:00
|
|
|
import { Reference } from "Conversations/Reference";
|
2026-07-10 20:23:44 +00:00
|
|
|
import { Artifact } from "Conversations/Artifact";
|
|
|
|
|
import type { IBinaryFile } from "Conversations/IBinaryFile";
|
|
|
|
|
import { arrayBufferToBase64, Notice } from "obsidian";
|
2026-02-15 22:46:25 +00:00
|
|
|
import { StringTools } from "Helpers/StringTools";
|
2025-09-30 20:20:24 +00:00
|
|
|
|
|
|
|
|
export class ConversationFileSystemService {
|
|
|
|
|
|
|
|
|
|
private fileSystemService: FileSystemService;
|
2025-12-19 12:30:51 +00:00
|
|
|
private aiFileService: IAIFileService | undefined;
|
|
|
|
|
|
2025-09-30 20:20:24 +00:00
|
|
|
private currentConversationPath: string | null = null;
|
2025-12-20 01:34:41 +00:00
|
|
|
private deletionQueue: Promise<void> = Promise.resolve();
|
2026-01-03 11:15:32 +00:00
|
|
|
private isDeleted: boolean = false;
|
2025-09-30 20:20:24 +00:00
|
|
|
|
|
|
|
|
public constructor() {
|
|
|
|
|
this.fileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 12:30:51 +00:00
|
|
|
public resolveAIFileService() {
|
|
|
|
|
this.aiFileService = Resolve<IAIFileService>(Services.IAIFileService);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 12:06:05 +00:00
|
|
|
public generateConversationPath(conversation: Conversation): string {
|
|
|
|
|
return `${Path.Conversations}/${conversation.title}.json`;
|
2025-09-30 20:20:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
public async saveConversation(conversation: Conversation): Promise<void> {
|
2026-01-03 11:15:32 +00:00
|
|
|
if (this.isDeleted) {
|
2026-07-10 20:23:44 +00:00
|
|
|
return;
|
2026-01-03 11:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-30 20:20:24 +00:00
|
|
|
if (!this.currentConversationPath) {
|
2025-10-05 12:06:05 +00:00
|
|
|
this.currentConversationPath = this.generateConversationPath(conversation);
|
2025-12-04 23:04:20 +00:00
|
|
|
} else {
|
|
|
|
|
// can happen if the conversation is deleted during an active request
|
|
|
|
|
const fileExists = await this.fileSystemService.exists(this.currentConversationPath, true);
|
|
|
|
|
if (!fileExists) {
|
2026-07-10 20:23:44 +00:00
|
|
|
return;
|
2025-12-04 23:04:20 +00:00
|
|
|
}
|
2025-09-30 20:20:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-22 09:06:32 +00:00
|
|
|
conversation.updated = new Date();
|
2026-02-15 22:46:25 +00:00
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
// Save binary files (attachments and artifacts) and update their storage paths
|
2026-02-15 22:46:25 +00:00
|
|
|
for (const content of conversation.contents) {
|
|
|
|
|
for (const attachment of content.attachments) {
|
2026-07-10 20:23:44 +00:00
|
|
|
await this.saveBinaryFile(attachment, Path.Attachments);
|
|
|
|
|
}
|
|
|
|
|
for (const artifact of content.artifacts) {
|
|
|
|
|
await this.saveBinaryFile(artifact, Path.Artifacts);
|
2026-02-15 22:46:25 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 20:20:24 +00:00
|
|
|
const conversationData = {
|
2025-10-04 12:10:22 +00:00
|
|
|
title: conversation.title,
|
|
|
|
|
created: conversation.created.toISOString(),
|
2025-10-22 09:06:32 +00:00
|
|
|
updated: conversation.updated.toISOString(),
|
2025-10-13 12:14:54 +00:00
|
|
|
contents: conversation.contents
|
|
|
|
|
.map(content => ({
|
|
|
|
|
role: content.role,
|
2025-12-19 12:30:51 +00:00
|
|
|
timestamp: content.timestamp.toISOString(),
|
2025-10-13 12:14:54 +00:00
|
|
|
content: content.content,
|
2025-12-19 12:30:51 +00:00
|
|
|
displayContent: content.displayContent,
|
2026-02-15 23:27:14 +00:00
|
|
|
toolCall: content.toolCall,
|
2025-12-19 12:30:51 +00:00
|
|
|
functionResponse: content.functionResponse,
|
2026-07-10 20:23:44 +00:00
|
|
|
artifacts: content.artifacts.map(artifact => ({
|
|
|
|
|
filePath: artifact.filePath,
|
|
|
|
|
mimeType: artifact.mimeType,
|
2026-07-11 12:49:44 +00:00
|
|
|
action: artifact.action,
|
2026-07-10 20:23:44 +00:00
|
|
|
originalContent: artifact.originalContent,
|
|
|
|
|
updatedContent: artifact.updatedContent,
|
|
|
|
|
artifactPath: artifact.artifactPath
|
|
|
|
|
})),
|
2026-02-15 22:46:25 +00:00
|
|
|
attachments: content.attachments.map(att => ({
|
|
|
|
|
fileName: att.fileName,
|
|
|
|
|
mimeType: att.mimeType,
|
|
|
|
|
filePath: att.filePath,
|
|
|
|
|
fileID: att.fileID
|
|
|
|
|
})),
|
2025-12-23 12:04:29 +00:00
|
|
|
references: content.references,
|
2025-12-19 12:30:51 +00:00
|
|
|
shouldDisplayContent: content.shouldDisplayContent,
|
2025-11-19 20:30:53 +00:00
|
|
|
toolId: content.toolId,
|
2025-12-10 21:27:58 +00:00
|
|
|
thoughtSignature: content.thoughtSignature,
|
2025-11-19 20:30:53 +00:00
|
|
|
errorType: content.errorType
|
2025-10-13 12:14:54 +00:00
|
|
|
}))
|
2025-09-30 20:20:24 +00:00
|
|
|
};
|
|
|
|
|
|
2025-11-24 21:29:54 +00:00
|
|
|
const result = await this.fileSystemService.writeObjectToFile(this.currentConversationPath, conversationData, true, false);
|
2025-11-17 19:02:15 +00:00
|
|
|
|
|
|
|
|
if (result instanceof Error) {
|
2026-07-10 20:23:44 +00:00
|
|
|
new Notice(`Failed to save conversation data for '${conversation.title}'`);
|
2025-11-17 19:02:15 +00:00
|
|
|
}
|
2025-09-30 20:20:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 19:02:15 +00:00
|
|
|
public resetCurrentConversation() {
|
2025-09-30 20:20:24 +00:00
|
|
|
this.currentConversationPath = null;
|
2026-01-03 11:15:32 +00:00
|
|
|
this.isDeleted = false;
|
2025-09-30 20:20:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 12:37:28 +00:00
|
|
|
public getCurrentConversationPath(): string | null {
|
|
|
|
|
return this.currentConversationPath;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 19:02:15 +00:00
|
|
|
public setCurrentConversationPath(filePath: string) {
|
2025-10-05 13:15:04 +00:00
|
|
|
this.currentConversationPath = filePath;
|
2026-01-03 11:15:32 +00:00
|
|
|
this.isDeleted = false;
|
2025-10-05 13:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 19:02:15 +00:00
|
|
|
public async deleteCurrentConversation(): Promise<void | Error> {
|
2025-10-03 21:14:30 +00:00
|
|
|
if (!this.currentConversationPath) {
|
2025-11-17 19:02:15 +00:00
|
|
|
return;
|
2025-10-03 21:14:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 12:30:51 +00:00
|
|
|
const readResult = await this.readConversation(this.currentConversationPath);
|
2025-10-03 21:14:30 +00:00
|
|
|
|
2025-12-19 12:30:51 +00:00
|
|
|
if (readResult instanceof Error) {
|
|
|
|
|
return readResult;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 01:34:41 +00:00
|
|
|
// Queue this to execute silently in the background - it's just a best effort
|
|
|
|
|
this.deletionQueue = this.deletionQueue.then(() => this.attemptAIFileDeletion(readResult));
|
|
|
|
|
|
2025-12-19 12:30:51 +00:00
|
|
|
const deleteResult = await this.fileSystemService.deleteFile(this.currentConversationPath, true, false);
|
|
|
|
|
|
|
|
|
|
if (deleteResult instanceof Error) {
|
|
|
|
|
return deleteResult;
|
2025-10-03 21:14:30 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-03 11:15:32 +00:00
|
|
|
// Mark as deleted to prevent subsequent saves during ongoing operations
|
|
|
|
|
this.isDeleted = true;
|
|
|
|
|
this.currentConversationPath = null;
|
2026-02-15 22:46:25 +00:00
|
|
|
|
|
|
|
|
// Queue garbage collection after AI file deletion
|
|
|
|
|
this.deletionQueue = this.deletionQueue.then(async () => {
|
|
|
|
|
await this.garbageCollectAttachments();
|
2026-07-10 20:23:44 +00:00
|
|
|
await this.garbageCollectArtifacts();
|
2026-02-15 22:46:25 +00:00
|
|
|
});
|
2025-10-03 21:14:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 12:06:05 +00:00
|
|
|
public async getAllConversations(): Promise<Conversation[]> {
|
2025-10-12 22:11:36 +00:00
|
|
|
const files = await this.fileSystemService.listFilesInDirectory(Path.Conversations, false, true);
|
2025-10-05 12:06:05 +00:00
|
|
|
const conversations: Conversation[] = [];
|
|
|
|
|
|
|
|
|
|
for (const file of files) {
|
2025-12-19 12:30:51 +00:00
|
|
|
const result = await this.readConversation(file.path);
|
|
|
|
|
if (result instanceof Conversation) {
|
|
|
|
|
conversations.push(result);
|
2025-10-05 12:06:05 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return conversations;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 22:46:25 +00:00
|
|
|
public async garbageCollectAttachments(): Promise<void | Error> {
|
|
|
|
|
try {
|
|
|
|
|
// 1. Get all attachment files
|
|
|
|
|
const attachmentFiles = await this.fileSystemService.listFilesInDirectory(
|
|
|
|
|
Path.Attachments,
|
|
|
|
|
false,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (attachmentFiles.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Build reference count map
|
|
|
|
|
const referenceCount = new Map<string, number>();
|
|
|
|
|
|
|
|
|
|
const conversations = await this.getAllConversations();
|
|
|
|
|
for (const conversation of conversations) {
|
|
|
|
|
for (const content of conversation.contents) {
|
|
|
|
|
for (const attachment of content.attachments) {
|
|
|
|
|
if (attachment.filePath) {
|
|
|
|
|
const count = referenceCount.get(attachment.filePath) || 0;
|
|
|
|
|
referenceCount.set(attachment.filePath, count + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Delete unreferenced files
|
|
|
|
|
for (const file of attachmentFiles) {
|
|
|
|
|
const relativePath = file.path.replace(`${Path.Conversations}/`, '');
|
|
|
|
|
const refCount = referenceCount.get(relativePath) || 0;
|
|
|
|
|
|
|
|
|
|
if (refCount === 0) {
|
|
|
|
|
const deleteResult = await this.fileSystemService.deleteFile(
|
|
|
|
|
file.path,
|
|
|
|
|
true,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (deleteResult instanceof Error) {
|
|
|
|
|
Exception.log(deleteResult);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Exception.log(error);
|
|
|
|
|
return Exception.new(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
public async garbageCollectArtifacts(): Promise<void | Error> {
|
|
|
|
|
try {
|
|
|
|
|
// 1. Get all artifact files
|
|
|
|
|
const artifactFiles = await this.fileSystemService.listFilesInDirectory(
|
|
|
|
|
Path.Artifacts,
|
|
|
|
|
false,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (artifactFiles.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Build reference count map
|
|
|
|
|
const referenceCount = new Map<string, number>();
|
|
|
|
|
|
|
|
|
|
const conversations = await this.getAllConversations();
|
|
|
|
|
for (const conversation of conversations) {
|
|
|
|
|
for (const content of conversation.contents) {
|
|
|
|
|
for (const artifact of content.artifacts) {
|
|
|
|
|
if (artifact.artifactPath) {
|
|
|
|
|
const count = referenceCount.get(artifact.artifactPath) || 0;
|
|
|
|
|
referenceCount.set(artifact.artifactPath, count + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Delete unreferenced files
|
|
|
|
|
for (const file of artifactFiles) {
|
|
|
|
|
const relativePath = file.path.replace(`${Path.Conversations}/`, '');
|
|
|
|
|
const refCount = referenceCount.get(relativePath) || 0;
|
|
|
|
|
|
|
|
|
|
if (refCount === 0) {
|
|
|
|
|
const deleteResult = await this.fileSystemService.deleteFile(
|
|
|
|
|
file.path,
|
|
|
|
|
true,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (deleteResult instanceof Error) {
|
|
|
|
|
Exception.log(deleteResult);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Exception.log(error);
|
|
|
|
|
return Exception.new(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 19:02:15 +00:00
|
|
|
public async updateConversationTitle(oldPath: string, newTitle: string): Promise<void | Error> {
|
2025-10-17 15:18:23 +00:00
|
|
|
const newPath = `${Path.Conversations}/${newTitle}.json`;
|
|
|
|
|
|
|
|
|
|
const result = await this.fileSystemService.moveFile(oldPath, newPath, true);
|
|
|
|
|
|
2025-11-17 19:02:15 +00:00
|
|
|
if (result instanceof Error) {
|
|
|
|
|
return result;
|
2025-10-17 15:18:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.currentConversationPath === oldPath) {
|
|
|
|
|
this.currentConversationPath = newPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
private async saveBinaryFile(file: IBinaryFile, storageFolder: Path): Promise<void> {
|
|
|
|
|
if (file.getStoragePath() || !file.base64) {
|
|
|
|
|
return;
|
2026-02-15 22:46:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
const hash = await StringTools.computeSHA256Hash(file.base64);
|
|
|
|
|
const fileName = `${hash}.bin`;
|
|
|
|
|
const filePath = `${storageFolder}/${fileName}`;
|
2026-02-15 22:46:25 +00:00
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
const exists = await this.fileSystemService.exists(filePath, true);
|
|
|
|
|
if (!exists) {
|
|
|
|
|
const arrayBuffer = StringTools.toBuffer(file.base64);
|
|
|
|
|
const result = await this.fileSystemService.writeBinaryFile(filePath, arrayBuffer, true);
|
2026-02-15 22:46:25 +00:00
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
if (result instanceof Error) {
|
|
|
|
|
Exception.log(result);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-15 22:46:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 14:14:44 +00:00
|
|
|
file.setStoragePath(filePath.replace(`${Path.Conversations}/`, ""));
|
2026-02-15 22:46:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
private async loadBinaryFile(storagePath: string): Promise<string> {
|
|
|
|
|
const fullPath = `${Path.Conversations}/${storagePath}`;
|
2026-02-15 22:46:25 +00:00
|
|
|
const arrayBuffer = await this.fileSystemService.readBinaryFile(fullPath, true);
|
|
|
|
|
|
|
|
|
|
if (arrayBuffer instanceof Error) {
|
|
|
|
|
Exception.log(arrayBuffer);
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return arrayBufferToBase64(arrayBuffer);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 12:30:51 +00:00
|
|
|
private async readConversation(path: string): Promise<Conversation | Error> {
|
|
|
|
|
const result = await this.fileSystemService.readObjectFromFile(path, true);
|
|
|
|
|
|
|
|
|
|
if (result instanceof Error) {
|
|
|
|
|
Exception.log(result);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const conversation: Conversation = new Conversation();
|
|
|
|
|
|
|
|
|
|
if (Conversation.isConversationData(result)) {
|
|
|
|
|
conversation.title = result.title;
|
|
|
|
|
conversation.created = new Date(result.created);
|
|
|
|
|
conversation.updated = new Date(result.updated);
|
2026-02-15 22:46:25 +00:00
|
|
|
|
|
|
|
|
const contentPromises = result.contents.map(async content => {
|
|
|
|
|
const attachments = await this.deserializeAttachments(content.attachments);
|
2025-12-23 12:04:29 +00:00
|
|
|
const references = this.deserializeReferences(content.references);
|
2026-07-10 20:23:44 +00:00
|
|
|
const artifacts = await this.deserializeArtifacts(content.artifacts);
|
2025-12-19 12:30:51 +00:00
|
|
|
|
|
|
|
|
return new ConversationContent({
|
|
|
|
|
role: content.role,
|
|
|
|
|
timestamp: new Date(content.timestamp),
|
|
|
|
|
content: content.content,
|
|
|
|
|
displayContent: content.displayContent,
|
2026-02-15 23:27:14 +00:00
|
|
|
toolCall: content.toolCall,
|
2025-12-19 12:30:51 +00:00
|
|
|
functionResponse: content.functionResponse,
|
2026-07-10 20:23:44 +00:00
|
|
|
artifacts: artifacts,
|
2025-12-19 12:30:51 +00:00
|
|
|
attachments: attachments,
|
2025-12-23 12:04:29 +00:00
|
|
|
references: references,
|
2025-12-19 12:30:51 +00:00
|
|
|
shouldDisplayContent: content.shouldDisplayContent,
|
|
|
|
|
toolId: content.toolId,
|
|
|
|
|
thoughtSignature: content.thoughtSignature,
|
|
|
|
|
errorType: content.errorType
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-02-15 22:46:25 +00:00
|
|
|
|
|
|
|
|
conversation.contents = await Promise.all(contentPromises);
|
2025-12-19 12:30:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return conversation;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 22:46:25 +00:00
|
|
|
private async deserializeAttachments(attachmentsData: unknown): Promise<Attachment[]> {
|
2025-12-19 12:30:51 +00:00
|
|
|
if (!Array.isArray(attachmentsData)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 22:46:25 +00:00
|
|
|
const attachments: Attachment[] = [];
|
|
|
|
|
|
|
|
|
|
for (const attachmentData of attachmentsData) {
|
|
|
|
|
if (!Attachment.isAttachmentData(attachmentData)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
const base64 = await this.loadBinaryFile(attachmentData.filePath);
|
2026-02-15 22:46:25 +00:00
|
|
|
|
|
|
|
|
if (!base64) {
|
|
|
|
|
Exception.warn(`Skipping attachment with missing file: ${attachmentData.fileName} (${attachmentData.filePath})`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const attachment = new Attachment(
|
2025-12-19 12:30:51 +00:00
|
|
|
attachmentData.fileName,
|
|
|
|
|
attachmentData.mimeType,
|
2026-02-15 22:46:25 +00:00
|
|
|
base64,
|
|
|
|
|
attachmentData.fileID || {},
|
|
|
|
|
attachmentData.filePath
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
attachments.push(attachment);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return attachments;
|
2025-12-19 12:30:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 20:23:44 +00:00
|
|
|
private async deserializeArtifacts(artifactsData: unknown): Promise<Artifact[]> {
|
|
|
|
|
if (!Array.isArray(artifactsData)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const artifacts: Artifact[] = [];
|
|
|
|
|
|
|
|
|
|
for (const artifactData of artifactsData) {
|
|
|
|
|
if (!Artifact.isArtifactData(artifactData)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const base64 = artifactData.artifactPath
|
|
|
|
|
? await this.loadBinaryFile(artifactData.artifactPath)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
artifacts.push(new Artifact(
|
|
|
|
|
artifactData.filePath,
|
|
|
|
|
artifactData.mimeType,
|
2026-07-11 12:49:44 +00:00
|
|
|
artifactData.action,
|
2026-07-10 20:23:44 +00:00
|
|
|
artifactData.originalContent,
|
|
|
|
|
artifactData.updatedContent,
|
|
|
|
|
base64,
|
|
|
|
|
artifactData.artifactPath
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return artifacts;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 12:04:29 +00:00
|
|
|
private deserializeReferences(referencesData: unknown): Reference[] {
|
|
|
|
|
if (!Array.isArray(referencesData)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return referencesData
|
|
|
|
|
.filter(Reference.isReferenceData)
|
|
|
|
|
.map(referenceData => new Reference(
|
|
|
|
|
referenceData.fileName,
|
|
|
|
|
referenceData.size
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 12:30:51 +00:00
|
|
|
private async attemptAIFileDeletion(conversation: Conversation) {
|
|
|
|
|
try {
|
|
|
|
|
await this.aiFileService?.refreshCache();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Exception.log(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const attachments = conversation.contents.map(c => c.attachments).flat();
|
|
|
|
|
for (const attachment of attachments) {
|
|
|
|
|
try {
|
|
|
|
|
await this.aiFileService?.deleteFile(attachment);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Exception.log(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 20:20:24 +00:00
|
|
|
}
|