feat: organize project conversations and add coding teacher side chat
This commit is contained in:
1 parent
2f82b9f79c
commit
9242a40d87
45 files changed
+2987
-1137
No files matched your search
@@ -0,0 +1,90 @@
|
||||
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';
|
||||
|
||||
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[0]?.text.slice(0, 32) || '新话题',
|
||||
updatedAt: topic.updatedAt,
|
||||
version: topic.version,
|
||||
}))
|
||||
.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;
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user