78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
import { mkdir, readFile, readdir } 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 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;
|
|
}
|
|
|
|
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";
|
|
}
|