Files
makelore/electron/coding-teacher/store.ts

118 lines
5.0 KiB
TypeScript

import { mkdir, readdir } from 'node:fs/promises';
import path from 'node:path';
import { atomicWriteJson, readJsonFile } from '../coding-projects/atomic-json';
import type { TeacherTopic, TeacherTopicList } from '../../shared/coding-teacher';
import { TeacherError } from './config-client';
import { parseTeacherDiscussionContent } from '../../shared/teacher-discussion';
export function teacherTopicId(id: string): string {
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(id))
throw new TeacherError(400, 'teacher_topic_invalid', '智能体话题标识无效。');
return id;
}
export class TeacherTopicStore {
private readonly cache = new Map<string, Promise<TeacherTopic>>();
constructor(readonly directory: string) {}
async list(): Promise<TeacherTopicList> {
await mkdir(this.directory, { recursive: true });
const names = await readdir(this.directory);
const topics = await Promise.all(
names
.filter((name) => name.endsWith('.json') && name !== 'index.json')
.map((name) => this.read(name.slice(0, -5)))
);
let last: string | null = null;
try {
last = (
(await readJsonFile(path.join(this.directory, 'index.json'))) as {
lastSelectedTopicId: string;
}
).lastSelectedTopicId;
} catch {
/* index is disposable; topic files own history */
}
const items = topics
.map((topic) => ({
id: topic.id,
title: topic.requests.find((request) => request.intent !== 'check-in')?.text.slice(0, 32)
|| (topic.requests.some((request) => request.intent === 'check-in') ? '和智能体聊聊' : '新话题'),
updatedAt: topic.updatedAt,
version: topic.version,
teacherId: topic.definition.config_id,
}))
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
return {
items,
lastSelectedTopicId: items.some((item) => item.id === last) ? last : (items[0]?.id ?? null),
};
}
async read(id: string): Promise<TeacherTopic> {
teacherTopicId(id);
let pending = this.cache.get(id);
if (!pending) {
pending = (async () => {
let topic: TeacherTopic;
try {
topic = (await readJsonFile(path.join(this.directory, id + '.json'))) as TeacherTopic;
} catch {
throw new TeacherError(404, 'teacher_topic_not_found', '智能体话题不存在或无法读取。');
}
if (topic.id !== id || topic.schemaVersion !== 1 || !Array.isArray(topic.requests))
throw new TeacherError(409, 'teacher_topic_invalid', '智能体历史无法读取。');
let recovered = false;
if (topic.discussion) {
try {
const discussion = topic.discussion;
teacherTopicId(discussion.id);
if (!Number.isSafeInteger(discussion.revision) || discussion.revision < 1
|| !['offered', 'active', 'paused', 'finished'].includes(discussion.status)) throw new Error('Invalid discussion');
discussion.content = parseTeacherDiscussionContent(discussion.content);
for (const key of ['previousIdeas', 'previousStructure'] as const) {
if (!discussion[key]) continue;
try {
const previous = parseTeacherDiscussionContent(discussion[key]);
if (key === 'previousIdeas' && previous.kind === 'ideas') discussion.previousIdeas = previous;
else if (key === 'previousStructure' && previous.kind === 'structure') discussion.previousStructure = previous;
else throw new Error('Invalid history');
} catch { delete discussion[key]; recovered = true; }
}
} catch { delete topic.discussion; recovered = true; }
}
for (const request of topic.requests) {
if (request.discussionSnapshot) {
try { request.discussionSnapshot = parseTeacherDiscussionContent(request.discussionSnapshot); }
catch { delete request.discussionSnapshot; recovered = true; }
}
}
for (const request of topic.requests)
if (request.status === 'preparing' || request.status === 'running') {
request.status = 'interrupted';
request.error = '应用已重启,本次回复中断。';
recovered = true;
}
if (recovered) {
topic.revision++;
await this.save(topic);
}
return topic;
})();
this.cache.set(id, pending);
pending.catch(() => this.cache.delete(id));
}
return await pending;
}
async save(topic: TeacherTopic) {
await mkdir(this.directory, { recursive: true });
const saved = { ...topic };
delete saved.unsaved;
await atomicWriteJson(path.join(this.directory, teacherTopicId(topic.id) + '.json'), saved);
topic.unsaved = false;
this.cache.set(topic.id, Promise.resolve(topic));
}
async select(id: string) {
await atomicWriteJson(path.join(this.directory, 'index.json'), {
lastSelectedTopicId: teacherTopicId(id),
});
}
}