import { mkdir, readFile, readdir, unlink } from "node:fs/promises"; import { join } from "node:path"; import type { Conversation, ConversationRepository, ConversationSummary, Message, } from "@great-agent/agent-core"; import { writeJsonAtomically } from "../atomic-writes/write-json-atomically"; import type { DataLayout } from "../layout/data-layout"; export class FileConversationRepository implements ConversationRepository { constructor(private readonly layout: DataLayout) {} async listRecent(): Promise { const conversations = await this.readAll(); return conversations .filter((conversation) => conversation.projectId === null) .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)) .map(({ messages: _messages, ...summary }) => summary); } async listByProject( projectId: string, ): Promise { return (await this.readAll()) .filter((conversation) => conversation.projectId === projectId) .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)) .map(({ messages: _messages, ...summary }) => summary); } async getById(id: string): Promise { try { return JSON.parse( await readFile(this.pathFor(id), "utf8"), ) as Conversation; } catch (error) { if (isFileNotFound(error)) return null; throw error; } } async create(conversation: Conversation): Promise { await mkdir(this.layout.conversations, { recursive: true }); if (await this.getById(conversation.id)) throw new Error("会话标识已经存在"); await writeJsonAtomically(this.pathFor(conversation.id), conversation); } async appendMessage( conversationId: string, message: Message, ): Promise { const current = await this.getById(conversationId); if (!current) throw new Error("会话不存在"); const next: Conversation = { ...current, messages: [...current.messages, message], updatedAt: message.createdAt, }; await writeJsonAtomically(this.pathFor(conversationId), next); return next; } async deleteByProject(projectId: string): Promise { const conversations = await this.readAll(); await Promise.all( conversations .filter((conversation) => conversation.projectId === projectId) .map((conversation) => unlink(this.pathFor(conversation.id))), ); } private pathFor(id: string): string { return join(this.layout.conversations, `${id}.json`); } private async readAll(): Promise { await mkdir(this.layout.conversations, { recursive: true }); const names = await readdir(this.layout.conversations); return Promise.all( names .filter((name) => name.endsWith(".json")) .map(async (name) => { return JSON.parse( await readFile(join(this.layout.conversations, name), "utf8"), ) as Conversation; }), ); } } function isFileNotFound(error: unknown): boolean { return error instanceof Error && "code" in error && error.code === "ENOENT"; }