一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
146 行
4.9 KiB
TypeScript
146 行
4.9 KiB
TypeScript
import { Mutex } from 'async-mutex';
|
|
import BaseModel, { ModelType } from '../BaseModel';
|
|
import markdownUtils from '../markdownUtils';
|
|
import { ChatMessage as ChatTurn, ChatRole, ChatToolMessage } from '../services/ai/types';
|
|
import ChatMessage from './ChatMessage';
|
|
|
|
export interface ChatHistoryMessage {
|
|
id: string;
|
|
createdTime: number;
|
|
noteId: string;
|
|
noteTitle: string;
|
|
role: 'user' | 'assistant' | 'error' | 'separator';
|
|
text: string;
|
|
raw: ChatTurn[];
|
|
hide?: boolean;
|
|
}
|
|
|
|
interface Conversation {
|
|
id: string;
|
|
title: string;
|
|
updated_time: number;
|
|
}
|
|
|
|
export default class ChatConversation extends BaseModel {
|
|
private static messageMutex_ = new Mutex();
|
|
|
|
public static tableName() {
|
|
return 'chat_conversations';
|
|
}
|
|
|
|
public static modelType() {
|
|
return ModelType.ChatConversation;
|
|
}
|
|
|
|
public static useUuid() {
|
|
return true;
|
|
}
|
|
|
|
public static async history(search = ''): Promise<Conversation[]> {
|
|
const searchQuery = search.trim().toLowerCase();
|
|
if (!searchQuery) {
|
|
return this.db().selectAll<Conversation>('SELECT id, title, updated_time FROM chat_conversations ORDER BY updated_time DESC');
|
|
}
|
|
|
|
return this.db().selectAll<Conversation>(`
|
|
SELECT id, title, updated_time FROM chat_conversations
|
|
WHERE instr(LOWER(title), ?) > 0
|
|
OR EXISTS (
|
|
SELECT 1 FROM chat_messages
|
|
WHERE chat_messages.conversation_id = chat_conversations.id
|
|
AND instr(LOWER(chat_messages.text), ?) > 0
|
|
)
|
|
ORDER BY updated_time DESC
|
|
`, [searchQuery, searchQuery]);
|
|
}
|
|
|
|
public static async renameConversation(id: string, title: string) {
|
|
await this.save({ id, title }, { autoTimestamp: false });
|
|
}
|
|
|
|
public static async deleteConversation(id: string) {
|
|
await ChatMessage.deleteByConversationId(id);
|
|
await this.delete(id);
|
|
}
|
|
|
|
public static async messages(conversationId: string): Promise<ChatHistoryMessage[]> {
|
|
const conversation = await this.load(conversationId, { fields: ['id'] });
|
|
if (!conversation) throw new Error(`No such chat conversation: ${conversationId}`);
|
|
const rows = await ChatMessage.byConversationId(conversationId);
|
|
return rows.map(row => ({
|
|
id: row.id,
|
|
createdTime: row.created_time,
|
|
role: row.role as ChatHistoryMessage['role'],
|
|
text: row.text,
|
|
raw: JSON.parse(row.raw),
|
|
hide: !!row.hide,
|
|
noteId: row.note_id,
|
|
noteTitle: row.note_title,
|
|
}));
|
|
}
|
|
|
|
public static async addMessage(conversationId: string, message: ChatHistoryMessage) {
|
|
const release = await this.messageMutex_.acquire();
|
|
try {
|
|
await this.saveForMessage(conversationId, message);
|
|
await ChatMessage.save({
|
|
id: message.id,
|
|
conversation_id: conversationId,
|
|
role: message.role,
|
|
text: message.text,
|
|
raw: JSON.stringify(message.raw),
|
|
hide: message.hide ? 1 : 0,
|
|
position: await ChatMessage.nextPosition(conversationId),
|
|
created_time: message.createdTime,
|
|
note_id: message.noteId,
|
|
note_title: message.noteTitle,
|
|
}, { isNew: true, autoTimestamp: false });
|
|
} finally {
|
|
release();
|
|
}
|
|
this.dispatch({ type: 'AI_CHAT_APPEND', conversationId, message });
|
|
}
|
|
|
|
public static async removeMessage(conversationId: string, id: string) {
|
|
const release = await this.messageMutex_.acquire();
|
|
try {
|
|
await ChatMessage.delete(id);
|
|
} finally {
|
|
release();
|
|
}
|
|
this.dispatch({ type: 'AI_CHAT_REMOVE', conversationId, id });
|
|
}
|
|
|
|
public static async addToolResult(conversationId: string, toolCall: ChatToolMessage) {
|
|
const release = await this.messageMutex_.acquire();
|
|
try {
|
|
for (const row of await ChatMessage.byToolCallId(conversationId, toolCall.toolCallId)) {
|
|
const raw: ChatTurn[] = JSON.parse(row.raw);
|
|
const isTarget = raw.some(entry => entry.role === ChatRole.Assistant && entry.toolCalls?.some(call => call.callId === toolCall.toolCallId));
|
|
if (!isTarget) continue;
|
|
raw.push(toolCall);
|
|
await ChatMessage.save({ id: row.id, raw: JSON.stringify(raw) }, { autoTimestamp: false });
|
|
break;
|
|
}
|
|
} finally {
|
|
release();
|
|
}
|
|
this.dispatch({ type: 'AI_CHAT_ADD_TOOL_RESULT', conversationId, toolCall });
|
|
}
|
|
|
|
private static async saveForMessage(conversationId: string, message: ChatHistoryMessage) {
|
|
const title = message.role === 'user' ? markdownUtils.titleFromBody(message.text) : '';
|
|
const bumpsUpdatedTime = message.role === 'user' || message.role === 'assistant';
|
|
const conversation = await this.load(conversationId, { fields: ['id', 'title', 'updated_time'] });
|
|
if (!conversation) {
|
|
await this.save({ id: conversationId, title, created_time: message.createdTime, updated_time: message.createdTime }, { isNew: true, autoTimestamp: false });
|
|
return;
|
|
}
|
|
|
|
const newTitle = conversation.title || title;
|
|
const newUpdatedTime = bumpsUpdatedTime ? Math.max(conversation.updated_time, message.createdTime) : conversation.updated_time;
|
|
if (newTitle === conversation.title && newUpdatedTime === conversation.updated_time) return;
|
|
await this.save({ id: conversationId, title: newTitle, updated_time: newUpdatedTime }, { autoTimestamp: false });
|
|
}
|
|
}
|