add local embed

This commit is contained in:
duanfuxiang
2025-07-04 09:28:12 +08:00
parent cd65d6b3de
commit 65c5df3d22
22 changed files with 2156 additions and 195 deletions

View File

@@ -5,6 +5,7 @@ import { ChatConversationMeta } from '../../../types/chat'
import { AbstractJsonRepository } from '../base'
import { CHAT_DIR, ROOT_DIR } from '../constants'
import { EmptyChatTitleException } from '../exception'
import { WorkspaceManager } from '../workspace/WorkspaceManager'
import {
CHAT_SCHEMA_VERSION,
@@ -16,27 +17,34 @@ export class ChatManager extends AbstractJsonRepository<
ChatConversation,
ChatConversationMeta
> {
constructor(app: App) {
private workspaceManager?: WorkspaceManager
constructor(app: App, workspaceManager?: WorkspaceManager) {
super(app, `${ROOT_DIR}/${CHAT_DIR}`)
this.workspaceManager = workspaceManager
}
protected generateFileName(chat: ChatConversation): string {
// Format: v{schemaVersion}_{title}_{updatedAt}_{id}.json
// 新格式: v{schemaVersion}_{title}_{updatedAt}_{id}_{workspaceId}.json
// 如果没有工作区,使用 'vault' 作为默认值
const encodedTitle = encodeURIComponent(chat.title)
return `v${chat.schemaVersion}_${encodedTitle}_${chat.updatedAt}_${chat.id}.json`
const workspaceId = chat.workspace || 'vault'
return `v${chat.schemaVersion}_${encodedTitle}_${chat.updatedAt}_${chat.id}_${workspaceId}.json`
}
protected parseFileName(fileName: string): ChatConversationMeta | null {
// Parse: v{schemaVersion}_{title}_{updatedAt}_{id}.json
// 使用一个正则表达式,工作区部分为可选: v{schemaVersion}_{title}_{updatedAt}_{id}_{workspaceId}?.json
const regex = new RegExp(
`^v${CHAT_SCHEMA_VERSION}_(.+)_(\\d+)_([0-9a-f-]+)\\.json$`,
`^v${CHAT_SCHEMA_VERSION}_(.+)_(\\d+)_([0-9a-f-]+)(?:_([^_]+))?\\.json$`,
)
const match = fileName.match(regex)
if (!match) return null
const title = decodeURIComponent(match[1])
const updatedAt = parseInt(match[2], 10)
const id = match[3]
const workspaceId = match[4] // 可能为undefined老格式
return {
id,
@@ -44,6 +52,8 @@ export class ChatManager extends AbstractJsonRepository<
title,
updatedAt,
createdAt: 0,
// 如果没有工作区信息老格式则认为是vault全局消息
workspace: workspaceId === 'vault' ? undefined : workspaceId,
}
}
@@ -66,6 +76,20 @@ export class ChatManager extends AbstractJsonRepository<
}
await this.create(newChat)
// 如果有工作区信息,添加到工作区的聊天历史中
if (newChat.workspace && this.workspaceManager) {
try {
await this.workspaceManager.addChatToWorkspace(
newChat.workspace,
newChat.id,
newChat.title
)
} catch (error) {
console.error('Failed to add chat to workspace:', error)
}
}
return newChat
}
@@ -102,6 +126,23 @@ export class ChatManager extends AbstractJsonRepository<
}
await this.update(chat, updatedChat)
// 如果标题或工作区发生变化,更新工作区的聊天历史
if (this.workspaceManager && (updates.title !== undefined || updates.workspace !== undefined)) {
const workspaceId = updatedChat.workspace || chat.workspace
if (workspaceId) {
try {
await this.workspaceManager.addChatToWorkspace(
workspaceId,
updatedChat.id,
updatedChat.title
)
} catch (error) {
console.error('Failed to update chat in workspace:', error)
}
}
}
return updatedChat
}
@@ -111,9 +152,22 @@ export class ChatManager extends AbstractJsonRepository<
if (targetsToDelete.length === 0) return false
// 获取聊天的工作区信息(从第一个匹配的元数据中获取)
const chatToDelete = await this.findById(id)
const workspaceId = chatToDelete?.workspace
// Delete all files associated with this ID
await Promise.all(targetsToDelete.map(meta => this.delete(meta.fileName)))
// 从工作区的聊天历史中移除
if (workspaceId && this.workspaceManager) {
try {
await this.workspaceManager.removeChatFromWorkspace(workspaceId, id)
} catch (error) {
console.error('Failed to remove chat from workspace:', error)
}
}
return true
}
@@ -126,7 +180,10 @@ export class ChatManager extends AbstractJsonRepository<
if (!chatsById.has(meta.id)) {
chatsById.set(meta.id, [])
}
chatsById.get(meta.id)!.push(meta)
const chatGroup = chatsById.get(meta.id)
if (chatGroup) {
chatGroup.push(meta)
}
}
const filesToDelete: string[] = []
@@ -151,11 +208,12 @@ export class ChatManager extends AbstractJsonRepository<
return filesToDelete.length
}
public async listChats(): Promise<ChatConversationMeta[]> {
public async listChats(workspaceFilter?: string): Promise<ChatConversationMeta[]> {
console.log('listChats', workspaceFilter)
const metadata = await this.listMetadata()
// Use a Map to store the latest version of each chat by ID.
const latestChats = new Map<string, ChatConversationMeta>()
const latestChats = new Map<string, ChatConversationMeta & { fileName: string }>()
for (const meta of metadata) {
const existing = latestChats.get(meta.id)
@@ -165,7 +223,27 @@ export class ChatManager extends AbstractJsonRepository<
}
const uniqueMetadata = Array.from(latestChats.values())
const sorted = uniqueMetadata.sort((a, b) => b.updatedAt - a.updatedAt)
// 将metadata转换为ChatConversationMeta格式
const chatMetadata: ChatConversationMeta[] = uniqueMetadata.map((meta) => ({
id: meta.id,
schemaVersion: meta.schemaVersion,
title: meta.title,
updatedAt: meta.updatedAt,
createdAt: meta.createdAt,
workspace: meta.workspace
}))
// 如果指定了工作区过滤器,则过滤对话
let filteredMetadata = chatMetadata
if (workspaceFilter !== undefined && workspaceFilter !== 'vault') {
// 获取指定工作区的对话
filteredMetadata = chatMetadata.filter(meta =>
meta.workspace === workspaceFilter
)
}
const sorted = filteredMetadata.sort((a, b) => b.updatedAt - a.updatedAt)
return sorted
}
}

View File

@@ -9,6 +9,7 @@ export type ChatConversation = {
createdAt: number
updatedAt: number
schemaVersion: number
workspace?: string // 工作区ID可选字段用于向后兼容
}
export type ChatConversationMetadata = {
@@ -16,4 +17,5 @@ export type ChatConversationMetadata = {
title: string
updatedAt: number
schemaVersion: number
workspace?: string // 工作区ID可选字段用于向后兼容
}

View File

@@ -5,181 +5,181 @@ import { AbstractJsonRepository } from '../base'
import { ROOT_DIR, WORKSPACE_DIR } from '../constants'
import {
WORKSPACE_SCHEMA_VERSION,
Workspace,
WorkspaceMetadata
WORKSPACE_SCHEMA_VERSION,
Workspace,
WorkspaceMetadata
} from './types'
export class WorkspaceManager extends AbstractJsonRepository<
Workspace,
WorkspaceMetadata
Workspace,
WorkspaceMetadata
> {
constructor(app: App) {
super(app, `${ROOT_DIR}/${WORKSPACE_DIR}`)
}
constructor(app: App) {
super(app, `${ROOT_DIR}/${WORKSPACE_DIR}`)
}
protected generateFileName(workspace: Workspace): string {
// Format: v{schemaVersion}_{name}_{updatedAt}_{id}.json
const encodedName = encodeURIComponent(workspace.name)
return `v${workspace.schemaVersion}_${encodedName}_${workspace.updatedAt}_${workspace.id}.json`
}
protected generateFileName(workspace: Workspace): string {
// Format: v{schemaVersion}_{name}_{updatedAt}_{id}.json
const encodedName = encodeURIComponent(workspace.name)
return `v${workspace.schemaVersion}_${encodedName}_${workspace.updatedAt}_${workspace.id}.json`
}
protected parseFileName(fileName: string): WorkspaceMetadata | null {
// Parse: v{schemaVersion}_{name}_{updatedAt}_{id}.json
const regex = new RegExp(
`^v${WORKSPACE_SCHEMA_VERSION}_(.+)_(\\d+)_([0-9a-f-]+)\\.json$`,
)
const match = fileName.match(regex)
if (!match) return null
protected parseFileName(fileName: string): WorkspaceMetadata | null {
// Parse: v{schemaVersion}_{name}_{updatedAt}_{id}.json
const regex = new RegExp(
`^v${WORKSPACE_SCHEMA_VERSION}_(.+)_(\\d+)_([0-9a-f-]+)\\.json$`,
)
const match = fileName.match(regex)
if (!match) return null
const name = decodeURIComponent(match[1])
const updatedAt = parseInt(match[2], 10)
const id = match[3]
const name = decodeURIComponent(match[1])
const updatedAt = parseInt(match[2], 10)
const id = match[3]
return {
id,
name,
updatedAt,
createdAt: 0,
schemaVersion: WORKSPACE_SCHEMA_VERSION,
}
}
return {
id,
name,
updatedAt,
createdAt: 0,
schemaVersion: WORKSPACE_SCHEMA_VERSION,
}
}
public async createWorkspace(
initialData: Partial<Workspace>,
): Promise<Workspace> {
const now = Date.now()
const newWorkspace: Workspace = {
id: uuidv4(),
name: 'New Workspace',
content: [],
chatHistory: [],
metadata: {},
createdAt: now,
updatedAt: now,
schemaVersion: WORKSPACE_SCHEMA_VERSION,
...initialData,
}
public async createWorkspace(
initialData: Partial<Workspace>,
): Promise<Workspace> {
const now = Date.now()
const newWorkspace: Workspace = {
id: uuidv4(),
name: 'New Workspace',
content: [],
chatHistory: [],
metadata: {},
createdAt: now,
updatedAt: now,
schemaVersion: WORKSPACE_SCHEMA_VERSION,
...initialData,
}
await this.create(newWorkspace)
return newWorkspace
}
await this.create(newWorkspace)
return newWorkspace
}
public async findById(id: string): Promise<Workspace | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.id === id)
public async findById(id: string): Promise<Workspace | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.id === id)
if (!targetMetadata) return null
if (!targetMetadata) return null
return this.read(targetMetadata.fileName)
}
return this.read(targetMetadata.fileName)
}
public async findByName(name: string): Promise<Workspace | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.name === name)
public async findByName(name: string): Promise<Workspace | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.name === name)
if (!targetMetadata) return null
if (!targetMetadata) return null
return this.read(targetMetadata.fileName)
}
return this.read(targetMetadata.fileName)
}
public async updateWorkspace(
id: string,
updates: Partial<
Omit<Workspace, 'id' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
>,
): Promise<Workspace | null> {
const workspace = await this.findById(id)
if (!workspace) return null
public async updateWorkspace(
id: string,
updates: Partial<
Omit<Workspace, 'id' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
>,
): Promise<Workspace | null> {
const workspace = await this.findById(id)
if (!workspace) return null
const updatedWorkspace: Workspace = {
...workspace,
...updates,
updatedAt: Date.now(),
}
const updatedWorkspace: Workspace = {
...workspace,
...updates,
updatedAt: Date.now(),
}
await this.update(workspace, updatedWorkspace)
return updatedWorkspace
}
await this.update(workspace, updatedWorkspace)
return updatedWorkspace
}
public async deleteWorkspace(id: string): Promise<boolean> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.id === id)
if (!targetMetadata) return false
public async deleteWorkspace(id: string): Promise<boolean> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.id === id)
if (!targetMetadata) return false
await this.delete(targetMetadata.fileName)
return true
}
await this.delete(targetMetadata.fileName)
return true
}
public async listWorkspaces(): Promise<WorkspaceMetadata[]> {
const metadata = await this.listMetadata()
const sorted = metadata.sort((a, b) => b.updatedAt - a.updatedAt)
return sorted
}
public async listWorkspaces(): Promise<WorkspaceMetadata[]> {
const metadata = await this.listMetadata()
const sorted = metadata.sort((a, b) => b.updatedAt - a.updatedAt)
return sorted
}
public async addChatToWorkspace(
workspaceId: string,
chatId: string,
chatTitle: string
): Promise<Workspace | null> {
const workspace = await this.findById(workspaceId)
if (!workspace) return null
public async addChatToWorkspace(
workspaceName: string,
chatId: string,
chatTitle: string
): Promise<Workspace | null> {
const workspace = await this.findByName(workspaceName)
if (!workspace) return null
const existingChatIndex = workspace.chatHistory.findIndex(
chat => chat.id === chatId
)
const existingChatIndex = workspace.chatHistory.findIndex(
chat => chat.id === chatId
)
if (existingChatIndex >= 0) {
// 更新已存在的聊天标题
workspace.chatHistory[existingChatIndex].title = chatTitle
} else {
// 添加新聊天
workspace.chatHistory.push({ id: chatId, title: chatTitle })
}
if (existingChatIndex >= 0) {
// 更新已存在的聊天标题
workspace.chatHistory[existingChatIndex].title = chatTitle
} else {
// 添加新聊天
workspace.chatHistory.push({ id: chatId, title: chatTitle })
}
return this.updateWorkspace(workspaceId, {
chatHistory: workspace.chatHistory
})
}
return this.updateWorkspace(workspace.id, {
chatHistory: workspace.chatHistory
})
}
public async removeChatFromWorkspace(
workspaceId: string,
chatId: string
): Promise<Workspace | null> {
const workspace = await this.findById(workspaceId)
if (!workspace) return null
public async removeChatFromWorkspace(
workspaceId: string,
chatId: string
): Promise<Workspace | null> {
const workspace = await this.findById(workspaceId)
if (!workspace) return null
workspace.chatHistory = workspace.chatHistory.filter(
chat => chat.id !== chatId
)
workspace.chatHistory = workspace.chatHistory.filter(
chat => chat.id !== chatId
)
return this.updateWorkspace(workspaceId, {
chatHistory: workspace.chatHistory
})
}
return this.updateWorkspace(workspaceId, {
chatHistory: workspace.chatHistory
})
}
public async ensureDefaultVaultWorkspace(): Promise<Workspace> {
// 检查是否已存在默认的 vault 工作区
const existingVault = await this.findByName('vault')
if (existingVault) {
return existingVault
}
public async ensureDefaultVaultWorkspace(): Promise<Workspace> {
// 检查是否已存在默认的 vault 工作区
const existingVault = await this.findByName('vault')
if (existingVault) {
return existingVault
}
// 创建默认的 vault 工作区
const defaultWorkspace = await this.createWorkspace({
name: 'vault',
content: [
{
type: 'folder',
content: '/' // 整个 vault 根目录
}
],
metadata: {
isDefault: true,
description: 'all vault as workspace'
}
})
// 创建默认的 vault 工作区
const defaultWorkspace = await this.createWorkspace({
name: 'vault',
content: [
{
type: 'folder',
content: '/' // 整个 vault 根目录
}
],
metadata: {
isDefault: true,
description: 'all vault as workspace'
}
})
return defaultWorkspace
}
return defaultWorkspace
}
}