fix: isolate delivered agent chats by project

This commit is contained in:
2026-09-28 16:21:14 +08:00
parent a21a1f077c
commit 4b41c23a3a
15 changed files with 518 additions and 165 deletions

View File

@@ -135,31 +135,32 @@ export class CodingTeacherService {
this.assertAccount(account);
return { items: items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)), lastSelectedTopicId: null };
}
private conversationStore(account: TeacherAccount, agentId: string) {
private conversationStore(account: TeacherAccount, projectId: string, agentId: string) {
if (!agentId || agentId.length > 128 || !/^[a-zA-Z0-9_-]+$/.test(agentId))
throw new TeacherError(400, 'teacher_agent_invalid', '智能体标识无效。');
const key = account.id + ':' + agentId;
teacherTopicId(projectId);
const key = account.id + ':' + projectId + ':' + agentId;
let store = this.conversations.get(key);
if (!store) {
store = new TeacherConversationStore(path.join(this.options.userDataDir, 'agent-conversations', account.id, agentId), account.id, agentId);
store = new TeacherConversationStore(path.join(this.options.userDataDir, 'agent-conversations', account.id, agentId, 'projects', projectId), account.id, agentId, projectId);
this.conversations.set(key, store);
}
return store;
}
private async importConversations(account: TeacherAccount, agentId: string) {
const store = this.conversationStore(account, agentId);
if (![...this.active.keys()].some(key => key.startsWith(account.id + ':agent:' + agentId + ':'))) {
for (const project of await this.options.projects.listProjects()) {
this.assertAccount(account);
await store.importProject(project);
}
private async importConversations(account: TeacherAccount, projectId: string, agentId: string) {
const project = await this.options.projects.getProject(projectId);
const store = this.conversationStore(account, projectId, agentId);
if (![...this.active.keys()].some(key => key.startsWith(account.id + ':agent:' + projectId + ':' + agentId + ':'))) {
this.assertAccount(account);
await store.importGlobal(path.join(this.options.userDataDir, 'agent-conversations', account.id, agentId));
await store.importProject(project);
}
return store;
}
async conversation(agentId: string, before?: string) {
async conversation(projectId: string, agentId: string, before?: string) {
const account = await this.account();
return this.serialize(account.id + ':agent:' + agentId + ':acceptance', async () => {
const store = await this.importConversations(account, agentId);
return this.serialize(this.acceptanceKey(account, { projectId, agentId, sourceId: 'project' }), async () => {
const store = await this.importConversations(account, projectId, agentId);
if (!await store.exists()) return { topic: null, before: null };
const topic = await store.read();
const page = await store.page(before);
@@ -172,7 +173,7 @@ export class CodingTeacherService {
const account = await this.account();
const scope: TeacherScope = { projectId: input.projectId, sourceId: 'project', agentId };
return this.serialize(this.acceptanceKey(account, scope), async () => {
const store = await this.importConversations(account, agentId);
const store = await this.importConversations(account, scope.projectId, agentId);
if (!await store.exists()) {
const catalog = await (this.options.catalog ?? teacherCatalog)(account);
const selected = catalog.items.find(item => item.teacher_id === agentId);
@@ -185,11 +186,11 @@ export class CodingTeacherService {
return this.sendRequest(account, scope, topic.id, input);
});
}
async markConversationSeen(agentId: string, requestId: string) {
async markConversationSeen(projectId: string, agentId: string, requestId: string) {
const account = await this.account();
const scope = { agentId, projectId: '', sourceId: 'project' };
const scope = { agentId, projectId, sourceId: 'project' };
return this.serialize(this.acceptanceKey(account, scope), async () => {
const store = this.conversationStore(account, agentId);
const store = this.conversationStore(account, projectId, agentId);
this.assertAccount(account);
await store.markSeen(requestId);
const topic = await store.read();
@@ -201,7 +202,7 @@ export class CodingTeacherService {
account: TeacherAccount,
scope: TeacherScope
): Promise<TeacherTopicStore | TeacherConversationStore> {
if (scope.agentId) return this.conversationStore(account, scope.agentId);
if (scope.agentId) return this.conversationStore(account, scope.projectId, scope.agentId);
if (this.deletingSources.has(scope.projectId + ':' + scope.sourceId))
throw new TeacherError(404, 'teacher_source_not_found', '来源会话已删除。');
let directory: string;
@@ -227,11 +228,11 @@ export class CodingTeacherService {
return store;
}
private key(account: TeacherAccount, scope: TeacherScope, id: string) {
if (scope.agentId) return account.id + ':agent:' + scope.agentId + ':' + id;
if (scope.agentId) return account.id + ':agent:' + scope.projectId + ':' + scope.agentId + ':' + id;
return account.id + ':' + scope.projectId + ':' + scope.sourceId + ':' + (scope.role ?? 'teacher') + ':' + id;
}
private acceptanceKey(account: TeacherAccount, scope: TeacherScope) {
if (scope.agentId) return account.id + ':agent:' + scope.agentId + ':acceptance';
if (scope.agentId) return this.key(account, scope, 'acceptance');
return account.id + ':' + scope.projectId + ':teacher-acceptance';
}
private async serialize<T>(key: string, operation: () => Promise<T>): Promise<T> {
@@ -317,8 +318,8 @@ export class CodingTeacherService {
const topic = (await store.read(id)) as PreviewTopic;
if (
topic.accountId !== account.id ||
(scope.agentId ? topic.conversation?.agentId !== scope.agentId :
topic.projectId !== scope.projectId || topic.sourceConversationId !== scope.sourceId) ||
topic.projectId !== scope.projectId ||
(scope.agentId ? topic.conversation?.agentId !== scope.agentId : topic.sourceConversationId !== scope.sourceId) ||
(topic.role ?? 'teacher') !== (scope.role ?? 'teacher')
)
throw new TeacherError(404, 'teacher_topic_not_found', '智能体话题不存在。');
@@ -712,6 +713,7 @@ export class CodingTeacherService {
const next = structuredClone(topic);
if (next.conversation) {
if (!input.projectId) throw new TeacherError(422, 'teacher_project_required', '请选择讨论所属的项目。');
if (input.projectId !== scope.projectId) throw new TeacherError(422, 'teacher_project_mismatch', '讨论不属于当前项目。');
await this.options.projects.getProject(input.projectId);
next.discussion = next.conversation.discussions[input.projectId];
}