llm-to-agent/packages/local-data/src/repositories/file-conversation-repository.ts
2026-08-14 14:34:27 +08:00

96 lines
3.1 KiB
TypeScript

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<readonly ConversationSummary[]> {
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<readonly ConversationSummary[]> {
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<Conversation | null> {
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<void> {
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<Conversation> {
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<void> {
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<Conversation[]> {
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";
}