mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 00:48:06 +00:00
feat(stage-*): store chat session to indexdb (#987)
* feat(stage-*): store chat session to indexdb * refactor: remove prompt hash * fix(stage-ui): enhance type guard for chat sessions export validation * refactor(stage-ui): simplify chat session export and import logic * refactor(stage-ui): update chat session management to use new data structure and improve export/import functionality * refactor(stage-ui): enhance chat session persistence and management methods * refactor(stage-ui): set and get idb value raw * fix: type
This commit is contained in:
@@ -4,6 +4,7 @@ import { themeColorFromValue, useThemeColor } from '@proj-airi/stage-layouts/com
|
||||
import { ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
@@ -33,6 +34,7 @@ const onboardingStore = useOnboardingStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const cardStore = useAiriCardStore()
|
||||
const chatSessionStore = useChatSessionStore()
|
||||
const serverChannelStore = useModsServerChannelStore()
|
||||
const characterOrchestratorStore = useCharacterOrchestratorStore()
|
||||
const analyticsStore = useSharedAnalyticsStore()
|
||||
@@ -53,6 +55,7 @@ onMounted(async () => {
|
||||
cardStore.initialize()
|
||||
onboardingStore.initializeSetupCheck()
|
||||
|
||||
await chatSessionStore.initialize()
|
||||
await displayModelsStore.loadDisplayModelsFromIndexedDB()
|
||||
await settingsStore.initializeStageModel()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { OnboardingDialog, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
@@ -28,6 +29,7 @@ const displayModelsStore = useDisplayModelsStore()
|
||||
const settingsStore = useSettings()
|
||||
const settings = storeToRefs(settingsStore)
|
||||
const onboardingStore = useOnboardingStore()
|
||||
const chatSessionStore = useChatSessionStore()
|
||||
const serverChannelStore = useModsServerChannelStore()
|
||||
const characterOrchestratorStore = useCharacterOrchestratorStore()
|
||||
const { shouldShowSetup } = storeToRefs(onboardingStore)
|
||||
@@ -76,6 +78,7 @@ onMounted(async () => {
|
||||
|
||||
onboardingStore.initializeSetupCheck()
|
||||
|
||||
await chatSessionStore.initialize()
|
||||
await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
|
||||
await contextBridgeStore.initialize()
|
||||
characterOrchestratorStore.initialize()
|
||||
|
||||
@@ -39,9 +39,9 @@ async function runAction(action: () => Promise<void> | void, successKey: string)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerExport() {
|
||||
async function triggerExport() {
|
||||
try {
|
||||
const blob = exportChatSessions()
|
||||
const blob = await exportChatSessions()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
@@ -70,7 +70,7 @@ async function handleImport(event: Event) {
|
||||
try {
|
||||
const raw = await file.text()
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
importChatSessions(parsed)
|
||||
await importChatSessions(parsed)
|
||||
setStatus(t('settings.pages.data.status.imported'))
|
||||
importError.value = ''
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChatHistoryItem } from '../types/chat'
|
||||
import type { ChatSessionsExport } from '../types/chat-session'
|
||||
|
||||
import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
@@ -63,21 +63,21 @@ export function useDataMaintenance() {
|
||||
chatStore.resetAllSessions()
|
||||
}
|
||||
|
||||
function exportChatSessions() {
|
||||
const data = chatStore.getAllSessions()
|
||||
async function exportChatSessions() {
|
||||
const data = await chatStore.exportSessions()
|
||||
return new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
|
||||
function importChatSessions(payload: Record<string, unknown>) {
|
||||
const normalizedPayload = payload as Record<string, unknown>
|
||||
const sessions: Record<string, ChatHistoryItem[]> = {}
|
||||
function isChatSessionsPayload(payload: unknown): payload is ChatSessionsExport {
|
||||
if (!payload || typeof payload !== 'object')
|
||||
return false
|
||||
return (payload as { format?: string }).format === 'chat-sessions-index:v1'
|
||||
}
|
||||
|
||||
for (const [sessionId, messages] of Object.entries(normalizedPayload)) {
|
||||
if (Array.isArray(messages))
|
||||
sessions[sessionId] = messages as ChatHistoryItem[]
|
||||
}
|
||||
|
||||
chatStore.replaceSessions(sessions)
|
||||
async function importChatSessions(payload: Record<string, unknown>) {
|
||||
if (!isChatSessionsPayload(payload))
|
||||
throw new Error('Invalid chat session export format')
|
||||
await chatStore.importSessions(payload)
|
||||
}
|
||||
|
||||
async function resetSettingsState() {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ChatSessionRecord, ChatSessionsIndex } from '../../types/chat-session'
|
||||
|
||||
import { storage } from '../storage'
|
||||
|
||||
export const chatSessionsRepo = {
|
||||
async getIndex(userId: string) {
|
||||
const key = `local:chat/index/${userId}`
|
||||
return await storage.getItemRaw<ChatSessionsIndex>(key)
|
||||
},
|
||||
|
||||
async saveIndex(index: ChatSessionsIndex) {
|
||||
const key = `local:chat/index/${index.userId}`
|
||||
await storage.setItemRaw(key, index)
|
||||
},
|
||||
|
||||
async getSession(sessionId: string) {
|
||||
const key = `local:chat/sessions/${sessionId}`
|
||||
return await storage.getItemRaw<ChatSessionRecord>(key)
|
||||
},
|
||||
|
||||
async saveSession(sessionId: string, record: ChatSessionRecord) {
|
||||
const key = `local:chat/sessions/${sessionId}`
|
||||
await storage.setItemRaw(key, record)
|
||||
},
|
||||
|
||||
// Cleanup
|
||||
async deleteSession(sessionId: string) {
|
||||
await storage.removeItem(`local:chat/sessions/${sessionId}`)
|
||||
},
|
||||
}
|
||||
@@ -29,6 +29,13 @@ interface SendOptions {
|
||||
input?: WebSocketEventInputs
|
||||
}
|
||||
|
||||
interface ForkOptions {
|
||||
fromSessionId?: string
|
||||
atIndex?: number
|
||||
reason?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
interface QueuedSend {
|
||||
sendingMessage: string
|
||||
options: SendOptions
|
||||
@@ -164,6 +171,7 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => {
|
||||
|
||||
const sessionMessagesForSend = chatSession.getSessionMessages(sessionId)
|
||||
sessionMessagesForSend.push({ role: 'user', content: finalContent })
|
||||
chatSession.persistSessionMessages(sessionId)
|
||||
|
||||
const categorizer = createStreamingCategorizer(activeProvider.value)
|
||||
let streamPosition = 0
|
||||
@@ -320,6 +328,7 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => {
|
||||
|
||||
if (!isStaleGeneration() && buildingMessage.slices.length > 0) {
|
||||
sessionMessagesForSend.push(toRaw(buildingMessage))
|
||||
chatSession.persistSessionMessages(sessionId)
|
||||
}
|
||||
|
||||
await hooks.emitStreamEndHooks(streamingMessageContext)
|
||||
@@ -365,6 +374,24 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function ingestOnFork(
|
||||
sendingMessage: string,
|
||||
options: SendOptions,
|
||||
forkOptions?: ForkOptions,
|
||||
) {
|
||||
const baseSessionId = forkOptions?.fromSessionId ?? activeSessionId.value
|
||||
if (!forkOptions)
|
||||
return ingest(sendingMessage, options, baseSessionId)
|
||||
|
||||
const forkSessionId = await chatSession.forkSession({
|
||||
fromSessionId: baseSessionId,
|
||||
atIndex: forkOptions.atIndex,
|
||||
reason: forkOptions.reason,
|
||||
hidden: forkOptions.hidden,
|
||||
})
|
||||
return ingest(sendingMessage, options, forkSessionId || baseSessionId)
|
||||
}
|
||||
|
||||
function cancelPendingSends(sessionId?: string) {
|
||||
for (const queued of pendingQueuedSends.value) {
|
||||
if (sessionId && queued.sessionId !== sessionId)
|
||||
@@ -385,6 +412,7 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => {
|
||||
discoverToolsCompatibility: llmStore.discoverToolsCompatibility,
|
||||
|
||||
ingest,
|
||||
ingestOnFork,
|
||||
cancelPendingSends,
|
||||
|
||||
clearHooks: hooks.clearHooks,
|
||||
|
||||
@@ -1,37 +1,58 @@
|
||||
import type { SystemMessage } from '@xsai/shared-chat'
|
||||
|
||||
import type { ChatHistoryItem } from '../../types/chat'
|
||||
import type { ChatSessionMeta, ChatSessionRecord, ChatSessionsExport, ChatSessionsIndex } from '../../types/chat-session'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useCharacterStore } from '../character'
|
||||
import { ACTIVE_SESSION_STORAGE_KEY, CHAT_STORAGE_KEY } from './constants'
|
||||
import { createChatDataStore } from './data-store'
|
||||
import { chatSessionsRepo } from '../../database/repos/chat-sessions.repo'
|
||||
import { useAuthStore } from '../auth'
|
||||
import { useAiriCardStore } from '../modules/airi-card'
|
||||
|
||||
export const useChatSessionStore = defineStore('chat-session', () => {
|
||||
const { systemPrompt } = storeToRefs(useCharacterStore())
|
||||
const { userId } = storeToRefs(useAuthStore())
|
||||
const { activeCardId, systemPrompt } = storeToRefs(useAiriCardStore())
|
||||
|
||||
const activeSessionId = useLocalStorage<string>(ACTIVE_SESSION_STORAGE_KEY, 'default')
|
||||
const sessionMessages = useLocalStorage<Record<string, ChatHistoryItem[]>>(CHAT_STORAGE_KEY, {})
|
||||
const activeSessionId = ref<string>('')
|
||||
const sessionMessages = ref<Record<string, ChatHistoryItem[]>>({})
|
||||
const sessionMetas = ref<Record<string, ChatSessionMeta>>({})
|
||||
const sessionGenerations = ref<Record<string, number>>({})
|
||||
const index = ref<ChatSessionsIndex | null>(null)
|
||||
|
||||
const dataStore = createChatDataStore({
|
||||
getActiveSessionId: () => activeSessionId.value,
|
||||
setActiveSessionId: sessionId => activeSessionId.value = sessionId,
|
||||
getSessions: () => sessionMessages.value,
|
||||
setSessions: sessions => sessionMessages.value = sessions,
|
||||
getGenerations: () => sessionGenerations.value,
|
||||
setGenerations: generations => sessionGenerations.value = generations,
|
||||
})
|
||||
const ready = ref(false)
|
||||
const isReady = computed(() => ready.value)
|
||||
const initializing = ref(false)
|
||||
let initializePromise: Promise<void> | null = null
|
||||
|
||||
let persistQueue = Promise.resolve()
|
||||
const loadedSessions = new Set<string>()
|
||||
const loadingSessions = new Map<string, Promise<void>>()
|
||||
|
||||
// I know this nu uh, better than loading all language on rehypeShiki
|
||||
const codeBlockSystemPrompt = '- For any programming code block, always specify the programming language that supported on @shikijs/rehype on the rendered markdown, eg. ```python ... ```\n'
|
||||
const mathSyntaxSystemPrompt = '- For any math equation, use LaTeX format, eg: $ x^3 $, always escape dollar sign outside math equation\n'
|
||||
|
||||
function generateInitialMessage() {
|
||||
const content = codeBlockSystemPrompt + mathSyntaxSystemPrompt + systemPrompt.value
|
||||
function getCurrentUserId() {
|
||||
return userId.value || 'local'
|
||||
}
|
||||
|
||||
function getCurrentCharacterId() {
|
||||
return activeCardId.value || 'default'
|
||||
}
|
||||
|
||||
function enqueuePersist(task: () => Promise<void>) {
|
||||
persistQueue = persistQueue.then(task, task)
|
||||
return persistQueue
|
||||
}
|
||||
|
||||
function snapshotMessages(messages: ChatHistoryItem[]) {
|
||||
return JSON.parse(JSON.stringify(messages)) as ChatHistoryItem[]
|
||||
}
|
||||
|
||||
function generateInitialMessageFromPrompt(prompt: string) {
|
||||
const content = codeBlockSystemPrompt + mathSyntaxSystemPrompt + prompt
|
||||
|
||||
return {
|
||||
role: 'system',
|
||||
@@ -39,55 +60,372 @@ export const useChatSessionStore = defineStore('chat-session', () => {
|
||||
} satisfies SystemMessage
|
||||
}
|
||||
|
||||
function ensureSession(sessionId: string) {
|
||||
dataStore.ensureSession(sessionId, generateInitialMessage)
|
||||
function generateInitialMessage() {
|
||||
return generateInitialMessageFromPrompt(systemPrompt.value)
|
||||
}
|
||||
|
||||
ensureSession(activeSessionId.value)
|
||||
function ensureGeneration(sessionId: string) {
|
||||
if (sessionGenerations.value[sessionId] === undefined)
|
||||
sessionGenerations.value[sessionId] = 0
|
||||
}
|
||||
|
||||
async function loadIndexForUser(currentUserId: string) {
|
||||
const stored = await chatSessionsRepo.getIndex(currentUserId)
|
||||
index.value = stored ?? {
|
||||
userId: currentUserId,
|
||||
characters: {},
|
||||
}
|
||||
}
|
||||
|
||||
function getCharacterIndex(characterId: string) {
|
||||
if (!index.value)
|
||||
return null
|
||||
return index.value.characters[characterId] ?? null
|
||||
}
|
||||
|
||||
async function persistIndex() {
|
||||
if (!index.value)
|
||||
return
|
||||
const snapshot = JSON.parse(JSON.stringify(index.value)) as ChatSessionsIndex
|
||||
await enqueuePersist(() => chatSessionsRepo.saveIndex(snapshot))
|
||||
}
|
||||
|
||||
async function persistSession(sessionId: string) {
|
||||
const meta = sessionMetas.value[sessionId]
|
||||
if (!meta)
|
||||
return
|
||||
const messages = snapshotMessages(sessionMessages.value[sessionId] ?? [])
|
||||
const now = Date.now()
|
||||
const updatedMeta = {
|
||||
...meta,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
sessionMetas.value[sessionId] = updatedMeta
|
||||
const characterIndex = index.value?.characters[meta.characterId]
|
||||
if (characterIndex)
|
||||
characterIndex.sessions[sessionId] = updatedMeta
|
||||
|
||||
const record: ChatSessionRecord = {
|
||||
meta: updatedMeta,
|
||||
messages,
|
||||
}
|
||||
|
||||
await enqueuePersist(() => chatSessionsRepo.saveSession(sessionId, record))
|
||||
await persistIndex()
|
||||
}
|
||||
|
||||
function persistSessionMessages(sessionId: string) {
|
||||
void persistSession(sessionId)
|
||||
}
|
||||
|
||||
function setSessionMessages(sessionId: string, next: ChatHistoryItem[]) {
|
||||
sessionMessages.value[sessionId] = next
|
||||
void persistSession(sessionId)
|
||||
}
|
||||
|
||||
async function loadSession(sessionId: string) {
|
||||
if (loadedSessions.has(sessionId))
|
||||
return
|
||||
if (loadingSessions.has(sessionId)) {
|
||||
await loadingSessions.get(sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
const loadPromise = (async () => {
|
||||
const stored = await chatSessionsRepo.getSession(sessionId)
|
||||
if (stored) {
|
||||
sessionMetas.value[sessionId] = stored.meta
|
||||
sessionMessages.value[sessionId] = stored.messages
|
||||
ensureGeneration(sessionId)
|
||||
}
|
||||
loadedSessions.add(sessionId)
|
||||
})()
|
||||
|
||||
loadingSessions.set(sessionId, loadPromise)
|
||||
await loadPromise
|
||||
loadingSessions.delete(sessionId)
|
||||
}
|
||||
|
||||
async function createSession(characterId: string, options?: { setActive?: boolean, messages?: ChatHistoryItem[], title?: string }) {
|
||||
const currentUserId = getCurrentUserId()
|
||||
const sessionId = nanoid()
|
||||
const now = Date.now()
|
||||
const meta: ChatSessionMeta = {
|
||||
sessionId,
|
||||
userId: currentUserId,
|
||||
characterId,
|
||||
title: options?.title,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
const initialMessages = options?.messages?.length ? options.messages : [generateInitialMessage()]
|
||||
|
||||
sessionMetas.value[sessionId] = meta
|
||||
sessionMessages.value[sessionId] = initialMessages
|
||||
ensureGeneration(sessionId)
|
||||
|
||||
if (!index.value)
|
||||
index.value = { userId: currentUserId, characters: {} }
|
||||
|
||||
const characterIndex = index.value.characters[characterId] ?? {
|
||||
activeSessionId: sessionId,
|
||||
sessions: {},
|
||||
}
|
||||
characterIndex.sessions[sessionId] = meta
|
||||
if (options?.setActive !== false)
|
||||
characterIndex.activeSessionId = sessionId
|
||||
index.value.characters[characterId] = characterIndex
|
||||
|
||||
await enqueuePersist(() => chatSessionsRepo.saveSession(sessionId, { meta, messages: initialMessages }))
|
||||
await persistIndex()
|
||||
|
||||
if (options?.setActive !== false)
|
||||
activeSessionId.value = sessionId
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
async function ensureActiveSessionForCharacter() {
|
||||
const currentUserId = getCurrentUserId()
|
||||
const characterId = getCurrentCharacterId()
|
||||
|
||||
if (!index.value || index.value.userId !== currentUserId)
|
||||
await loadIndexForUser(currentUserId)
|
||||
|
||||
const characterIndex = getCharacterIndex(characterId)
|
||||
if (!characterIndex) {
|
||||
await createSession(characterId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!characterIndex.activeSessionId) {
|
||||
await createSession(characterId)
|
||||
return
|
||||
}
|
||||
|
||||
activeSessionId.value = characterIndex.activeSessionId
|
||||
await loadSession(characterIndex.activeSessionId)
|
||||
ensureSession(characterIndex.activeSessionId)
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
if (ready.value)
|
||||
return
|
||||
if (initializePromise)
|
||||
return initializePromise
|
||||
initializing.value = true
|
||||
initializePromise = (async () => {
|
||||
await ensureActiveSessionForCharacter()
|
||||
ready.value = true
|
||||
})()
|
||||
|
||||
try {
|
||||
await initializePromise
|
||||
}
|
||||
finally {
|
||||
initializePromise = null
|
||||
initializing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSession(sessionId: string) {
|
||||
ensureGeneration(sessionId)
|
||||
if (!sessionMessages.value[sessionId] || sessionMessages.value[sessionId].length === 0) {
|
||||
sessionMessages.value[sessionId] = [generateInitialMessage()]
|
||||
void persistSession(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
const messages = computed<ChatHistoryItem[]>({
|
||||
get: () => dataStore.getSessionMessages(activeSessionId.value, generateInitialMessage),
|
||||
set: value => dataStore.setSessionMessages(activeSessionId.value, value),
|
||||
get: () => {
|
||||
if (!activeSessionId.value)
|
||||
return []
|
||||
ensureSession(activeSessionId.value)
|
||||
if (ready.value)
|
||||
void loadSession(activeSessionId.value)
|
||||
return sessionMessages.value[activeSessionId.value] ?? []
|
||||
},
|
||||
set: (value) => {
|
||||
if (!activeSessionId.value)
|
||||
return
|
||||
sessionMessages.value[activeSessionId.value] = value
|
||||
void persistSession(activeSessionId.value)
|
||||
},
|
||||
})
|
||||
|
||||
function setActiveSession(sessionId: string) {
|
||||
dataStore.setActiveSession(sessionId, generateInitialMessage)
|
||||
activeSessionId.value = sessionId
|
||||
ensureSession(sessionId)
|
||||
|
||||
const characterId = getCurrentCharacterId()
|
||||
const characterIndex = index.value?.characters[characterId]
|
||||
if (characterIndex) {
|
||||
characterIndex.activeSessionId = sessionId
|
||||
void persistIndex()
|
||||
}
|
||||
|
||||
if (ready.value)
|
||||
void loadSession(sessionId)
|
||||
}
|
||||
|
||||
function cleanupMessages(sessionId = activeSessionId.value) {
|
||||
dataStore.resetSession(sessionId, generateInitialMessage)
|
||||
ensureGeneration(sessionId)
|
||||
sessionGenerations.value[sessionId] += 1
|
||||
setSessionMessages(sessionId, [generateInitialMessage()])
|
||||
}
|
||||
|
||||
function getAllSessions() {
|
||||
return dataStore.getAllSessions()
|
||||
return JSON.parse(JSON.stringify(sessionMessages.value)) as Record<string, ChatHistoryItem[]>
|
||||
}
|
||||
|
||||
function replaceSessions(sessions: Record<string, ChatHistoryItem[]>) {
|
||||
dataStore.replaceSessions(sessions, generateInitialMessage)
|
||||
async function resetAllSessions() {
|
||||
const currentUserId = getCurrentUserId()
|
||||
const characterId = getCurrentCharacterId()
|
||||
const sessionIds = new Set<string>()
|
||||
|
||||
if (index.value?.userId === currentUserId) {
|
||||
for (const character of Object.values(index.value.characters)) {
|
||||
for (const sessionId of Object.keys(character.sessions))
|
||||
sessionIds.add(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const sessionId of sessionIds)
|
||||
await enqueuePersist(() => chatSessionsRepo.deleteSession(sessionId))
|
||||
|
||||
sessionMessages.value = {}
|
||||
sessionMetas.value = {}
|
||||
sessionGenerations.value = {}
|
||||
loadedSessions.clear()
|
||||
loadingSessions.clear()
|
||||
|
||||
index.value = {
|
||||
userId: currentUserId,
|
||||
characters: {},
|
||||
}
|
||||
|
||||
await createSession(characterId)
|
||||
}
|
||||
|
||||
function resetAllSessions() {
|
||||
dataStore.resetAllSessions(generateInitialMessage)
|
||||
function getSessionMessages(sessionId: string) {
|
||||
ensureSession(sessionId)
|
||||
if (ready.value)
|
||||
void loadSession(sessionId)
|
||||
return sessionMessages.value[sessionId] ?? []
|
||||
}
|
||||
|
||||
watch(systemPrompt, () => {
|
||||
dataStore.refreshSystemMessages(generateInitialMessage)
|
||||
}, { immediate: true })
|
||||
function getSessionGeneration(sessionId: string) {
|
||||
ensureGeneration(sessionId)
|
||||
return sessionGenerations.value[sessionId] ?? 0
|
||||
}
|
||||
|
||||
function bumpSessionGeneration(sessionId: string) {
|
||||
ensureGeneration(sessionId)
|
||||
sessionGenerations.value[sessionId] += 1
|
||||
return sessionGenerations.value[sessionId]
|
||||
}
|
||||
|
||||
function getSessionGenerationValue(sessionId?: string) {
|
||||
const target = sessionId ?? activeSessionId.value
|
||||
return getSessionGeneration(target)
|
||||
}
|
||||
|
||||
async function forkSession(options: { fromSessionId: string, atIndex?: number, reason?: string, hidden?: boolean }) {
|
||||
const characterId = getCurrentCharacterId()
|
||||
const parentMessages = getSessionMessages(options.fromSessionId)
|
||||
const forkIndex = options.atIndex ?? parentMessages.length
|
||||
const nextMessages = parentMessages.slice(0, forkIndex)
|
||||
return await createSession(characterId, { setActive: false, messages: nextMessages })
|
||||
}
|
||||
|
||||
async function exportSessions(): Promise<ChatSessionsExport> {
|
||||
if (!ready.value)
|
||||
await initialize()
|
||||
|
||||
if (!index.value) {
|
||||
return {
|
||||
format: 'chat-sessions-index:v1',
|
||||
index: { userId: getCurrentUserId(), characters: {} },
|
||||
sessions: {},
|
||||
}
|
||||
}
|
||||
|
||||
const sessions: Record<string, ChatSessionRecord> = {}
|
||||
for (const character of Object.values(index.value.characters)) {
|
||||
for (const sessionId of Object.keys(character.sessions)) {
|
||||
const stored = await chatSessionsRepo.getSession(sessionId)
|
||||
if (stored) {
|
||||
sessions[sessionId] = stored
|
||||
continue
|
||||
}
|
||||
const meta = sessionMetas.value[sessionId]
|
||||
const messages = sessionMessages.value[sessionId]
|
||||
if (meta && messages)
|
||||
sessions[sessionId] = { meta, messages }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
format: 'chat-sessions-index:v1',
|
||||
index: index.value,
|
||||
sessions,
|
||||
}
|
||||
}
|
||||
|
||||
async function importSessions(payload: ChatSessionsExport) {
|
||||
if (payload.format !== 'chat-sessions-index:v1')
|
||||
return
|
||||
|
||||
index.value = payload.index
|
||||
sessionMessages.value = {}
|
||||
sessionMetas.value = {}
|
||||
sessionGenerations.value = {}
|
||||
loadedSessions.clear()
|
||||
loadingSessions.clear()
|
||||
|
||||
await enqueuePersist(() => chatSessionsRepo.saveIndex(payload.index))
|
||||
|
||||
for (const [sessionId, record] of Object.entries(payload.sessions)) {
|
||||
sessionMetas.value[sessionId] = record.meta
|
||||
sessionMessages.value[sessionId] = record.messages
|
||||
ensureGeneration(sessionId)
|
||||
await enqueuePersist(() => chatSessionsRepo.saveSession(sessionId, record))
|
||||
}
|
||||
|
||||
await ensureActiveSessionForCharacter()
|
||||
}
|
||||
|
||||
watch([userId, activeCardId], () => {
|
||||
if (!ready.value)
|
||||
return
|
||||
void ensureActiveSessionForCharacter()
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
isReady,
|
||||
initialize,
|
||||
|
||||
activeSessionId,
|
||||
messages,
|
||||
|
||||
setActiveSession,
|
||||
cleanupMessages,
|
||||
getAllSessions,
|
||||
replaceSessions,
|
||||
resetAllSessions,
|
||||
|
||||
ensureSession,
|
||||
getSessionMessages: (sessionId: string) => dataStore.getSessionMessages(sessionId, generateInitialMessage),
|
||||
getSessionGeneration: (sessionId: string) => dataStore.getSessionGeneration(sessionId),
|
||||
bumpSessionGeneration: (sessionId: string) => dataStore.bumpSessionGeneration(sessionId),
|
||||
getSessionGenerationValue: (sessionId?: string) => dataStore.getSessionGenerationValue(sessionId),
|
||||
setSessionMessages,
|
||||
persistSessionMessages,
|
||||
getSessionMessages,
|
||||
getSessionGeneration,
|
||||
bumpSessionGeneration,
|
||||
getSessionGenerationValue,
|
||||
|
||||
forkSession,
|
||||
exportSessions,
|
||||
importSessions,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ export const useChatStreamStore = defineStore('chat-stream', () => {
|
||||
const sessionMessagesForSend = chatSession.getSessionMessages(sessionId)
|
||||
if (streamingMessage.value.slices.length > 0)
|
||||
sessionMessagesForSend.push(streamingMessage.value)
|
||||
chatSession.persistSessionMessages(sessionId)
|
||||
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [] }
|
||||
if (fullText)
|
||||
streamingMessage.value.content = fullText
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ChatHistoryItem } from './chat'
|
||||
|
||||
export interface ChatSessionMeta {
|
||||
sessionId: string
|
||||
userId: string
|
||||
characterId: string
|
||||
title?: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface ChatSessionRecord {
|
||||
meta: ChatSessionMeta
|
||||
messages: ChatHistoryItem[]
|
||||
}
|
||||
|
||||
export interface ChatCharacterSessionsIndex {
|
||||
activeSessionId: string
|
||||
sessions: Record<string, ChatSessionMeta>
|
||||
}
|
||||
|
||||
export interface ChatSessionsIndex {
|
||||
userId: string
|
||||
characters: Record<string, ChatCharacterSessionsIndex>
|
||||
}
|
||||
|
||||
export interface ChatSessionsExport {
|
||||
format: 'chat-sessions-index:v1'
|
||||
index: ChatSessionsIndex
|
||||
sessions: Record<string, ChatSessionRecord>
|
||||
}
|
||||
Reference in New Issue
Block a user