mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-09 00:20:09 +00:00
add local embed
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,可选字段用于向后兼容
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user