UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

245 lines (193 loc) 7.58 kB
import { BaseModel } from '@/database/_deprecated/core'; import { DBModel } from '@/database/_deprecated/core/types/db'; import { MessageModel } from '@/database/_deprecated/models/message'; import { DB_Topic, DB_TopicSchema } from '@/database/_deprecated/schemas/topic'; import { ChatTopic } from '@/types/topic'; import { nanoid } from '@/utils/uuid'; export interface CreateTopicParams { favorite?: boolean; messages?: string[]; sessionId: string; title: string; } export interface QueryTopicParams { current?: number; pageSize?: number; sessionId: string; } class _TopicModel extends BaseModel { constructor() { super('topics', DB_TopicSchema); } // **************** Query *************** // async query({ pageSize = 9999, current = 0, sessionId }: QueryTopicParams): Promise<ChatTopic[]> { const offset = current * pageSize; // get all topics const allTopics = await this.table.where('sessionId').equals(sessionId).toArray(); // 将所有主题按星标消息优先,时间倒序进行排序 const sortedTopics = allTopics.sort((a, b) => { if (a.favorite && !b.favorite) return -1; // a是星标,b不是,a排前面 if (!a.favorite && b.favorite) return 1; // b是星标,a不是,b排前面 // 如果星标状态相同,则按时间倒序排序 return b.createdAt - a.createdAt; }); // handle pageSize const pagedTopics = sortedTopics.slice(offset, offset + pageSize); return pagedTopics.map((i) => this.mapToChatTopic(i)); } queryAll() { return this.table.orderBy('updatedAt').toArray(); } /** * Query topics by keyword in title, message content, or translated content * @param keyword The keyword to search for * @param sessionId The currently activated session id. */ async queryByKeyword(keyword: string, sessionId?: string): Promise<ChatTopic[]> { if (!keyword) return []; console.time('queryTopicsByKeyword'); const keywordLowerCase = keyword.toLowerCase(); // Find topics with matching title const queryTable = sessionId ? this.table.where('sessionId').equals(sessionId) : this.table; const matchingTopicsPromise = queryTable .filter((topic) => topic.title.toLowerCase().includes(keywordLowerCase)) .toArray(); // Find messages with matching content or translate.content const queryMessages = sessionId ? this.db.messages.where('sessionId').equals(sessionId) : this.db.messages; const matchingMessagesPromise = queryMessages .filter((message) => { // check content if (message.content.toLowerCase().includes(keywordLowerCase)) return true; // check translate content if (message.translate && message.translate.content) { return message.translate.content.toLowerCase().includes(keywordLowerCase); } return false; }) .toArray(); // Resolve both promises const [matchingTopics, matchingMessages] = await Promise.all([ matchingTopicsPromise, matchingMessagesPromise, ]); // Extract topic IDs from messages const topicIdsFromMessages = matchingMessages.map((message) => message.topicId); // Combine topic IDs from both sources const combinedTopicIds = new Set([ ...topicIdsFromMessages, ...matchingTopics.map((topic) => topic.id), ]); // Retrieve unique topics by IDs const uniqueTopics = await this.table .where('id') .anyOf([...combinedTopicIds]) .toArray(); console.timeEnd('queryTopicsByKeyword'); return uniqueTopics.map((i) => ({ ...i, favorite: !!i.favorite })); } async findBySessionId(sessionId: string) { return this.table.where({ sessionId }).toArray(); } async findById(id: string): Promise<DBModel<DB_Topic>> { return this.table.get(id); } async count() { return this.table.count(); } // **************** Create *************** // async create({ title, favorite, sessionId, messages }: CreateTopicParams, id = nanoid()) { const topic = await this._addWithSync( { favorite: favorite ? 1 : 0, sessionId, title: title }, id, ); // add topicId to these messages if (messages) { await MessageModel.batchUpdate(messages, { topicId: topic.id }); } return topic; } async batchCreate(topics: CreateTopicParams[]) { return this._batchAdd(topics.map((t) => ({ ...t, favorite: t.favorite ? 1 : 0 }))); } async duplicateTopic(topicId: string, newTitle?: string) { return this.db.transaction('rw', [this.db.topics, this.db.messages], async () => { // Step 1: get DB_Topic const topic = await this.findById(topicId); if (!topic) { throw new Error(`Topic with id ${topicId} not found`); } // Step 3: 查询与 `topic` 关联的 `messages` const originalMessages = await MessageModel.queryByTopicId(topicId); const duplicateMessages = await MessageModel.duplicateMessages(originalMessages); const { id } = await this.create({ ...this.mapToChatTopic(topic), messages: duplicateMessages.map((m) => m.id), sessionId: topic.sessionId!, title: newTitle || topic.title, }); return id; }); } // **************** Delete *************** // /** * Deletes a topic and all messages associated with it. */ async delete(id: string) { return this.db.transaction('rw', [this.table, this.db.messages], async () => { // Delete all messages associated with the topic await MessageModel.batchDeleteByTopicId(id); await this._deleteWithSync(id); }); } /** * Deletes multiple topic based on the sessionId. * * @param {string} sessionId - The identifier of the assistant associated with the messages. * @returns {Promise<void>} */ async batchDeleteBySessionId(sessionId: string): Promise<void> { // use sessionId as the filter criteria in the query. const query = this.table.where('sessionId').equals(sessionId); // Retrieve a collection of message IDs that satisfy the criteria const topicIds = await query.primaryKeys(); // Use the bulkDelete method to delete all selected messages in bulk return this._bulkDeleteWithSync(topicIds); } /** * Deletes multiple topics and all messages associated with them in a transaction. */ async batchDelete(topicIds: string[]) { return this.db.transaction('rw', [this.table, this.db.messages], async () => { // Iterate over each topicId and delete related messages, then delete the topic itself for (const topicId of topicIds) { // Delete all messages associated with the topic await this.delete(topicId); } }); } async clearTable() { return this._clearWithSync(); } // **************** Update *************** // async update(id: string, data: Partial<DB_Topic>) { return super._updateWithSync(id, data); } async toggleFavorite(id: string, newState?: boolean) { const topic = await this.findById(id); if (!topic) { throw new Error(`Topic with id ${id} not found`); } // Toggle the 'favorite' status const nextState = typeof newState !== 'undefined' ? newState : !topic.favorite; await this.update(id, { favorite: nextState ? 1 : 0 }); return nextState; } // **************** Helper *************** // private mapToChatTopic = (dbTopic: DBModel<DB_Topic>): ChatTopic => ({ ...dbTopic, favorite: !!dbTopic.favorite, }); } export const TopicModel = new _TopicModel();