update workspace

This commit is contained in:
duanfuxiang
2025-06-27 22:08:36 +08:00
parent 0df4e4edd3
commit 772270863c
86 changed files with 6988 additions and 1156 deletions

View File

@@ -3,4 +3,5 @@ export const COMMAND_DIR = 'commands'
export const CHAT_DIR = 'chats'
export const CUSTOM_MODE_DIR = 'custom_modes'
export const CONVERT_DATA_DIR = 'convert_data'
export const WORKSPACE_DIR = 'workspaces'
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'

View File

@@ -0,0 +1,185 @@
import { App } from 'obsidian'
import { v4 as uuidv4 } from 'uuid'
import { AbstractJsonRepository } from '../base'
import { ROOT_DIR, WORKSPACE_DIR } from '../constants'
import {
WORKSPACE_SCHEMA_VERSION,
Workspace,
WorkspaceMetadata
} from './types'
export class WorkspaceManager extends AbstractJsonRepository<
Workspace,
WorkspaceMetadata
> {
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 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]
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,
}
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)
if (!targetMetadata) return null
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)
if (!targetMetadata) return null
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
const updatedWorkspace: Workspace = {
...workspace,
...updates,
updatedAt: Date.now(),
}
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
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 addChatToWorkspace(
workspaceId: string,
chatId: string,
chatTitle: string
): Promise<Workspace | null> {
const workspace = await this.findById(workspaceId)
if (!workspace) return null
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 })
}
return this.updateWorkspace(workspaceId, {
chatHistory: workspace.chatHistory
})
}
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
)
return this.updateWorkspace(workspaceId, {
chatHistory: workspace.chatHistory
})
}
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'
}
})
return defaultWorkspace
}
}

View File

@@ -0,0 +1,2 @@
export * from './types'
export * from './WorkspaceManager'

View File

@@ -0,0 +1,30 @@
export const WORKSPACE_SCHEMA_VERSION = 1
export interface WorkspaceContent {
type: 'tag' | 'folder'
content: string
}
export interface WorkspaceChatHistory {
id: string
title: string
}
export interface Workspace {
id: string
name: string
content: WorkspaceContent[]
chatHistory: WorkspaceChatHistory[]
metadata: Record<string, any>
createdAt: number
updatedAt: number
schemaVersion: number
}
export interface WorkspaceMetadata {
id: string
name: string
updatedAt: number
createdAt: number
schemaVersion: number
}

View File

@@ -1,5 +1,5 @@
import { backOff } from 'exponential-backoff'
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
import { MarkdownTextSplitter } from 'langchain/text_splitter'
import { minimatch } from 'minimatch'
import { App, Notice, TFile } from 'obsidian'
import pLimit from 'p-limit'
@@ -111,17 +111,10 @@ export class VectorManager {
return
}
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage(
'markdown',
{
chunkSize: options.chunkSize,
// TODO: Use token-based chunking after migrating to WebAssembly-based tiktoken
// Current token counting method is too slow for practical use
// lengthFunction: async (text) => {
// return await tokenCount(text)
// },
},
)
const textSplitter = new MarkdownTextSplitter({
chunkSize: options.chunkSize,
chunkOverlap: Math.floor(options.chunkSize * 0.15)
})
const skippedFiles: string[] = []
const contentChunks: InsertVector[] = (
@@ -323,12 +316,10 @@ export class VectorManager {
)
// Embed the files
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage(
'markdown',
{
chunkSize,
},
)
const textSplitter = new MarkdownTextSplitter({
chunkSize: chunkSize,
chunkOverlap: Math.floor(chunkSize * 0.15)
});
let fileContent = await this.app.vault.cachedRead(file)
// 清理null字节防止PostgreSQL UTF8编码错误
fileContent = fileContent.replace(/\0/g, '')