diff --git a/docs/builtin-knowledge-architecture.md b/docs/builtin-knowledge-architecture.md new file mode 100644 index 000000000..bc14cad6e --- /dev/null +++ b/docs/builtin-knowledge-architecture.md @@ -0,0 +1,109 @@ +# BuiltinKnowledge 架构文档 + +## 模块概述 + +BuiltinKnowledge(内置知识库)模块是 DeepChat 中负责本地知识存储、检索与管理的核心组件,主要功能包括: + +1. **知识库生命周期管理**:创建、重置、删除、文件增删查、RAG 实例管理。 +2. **配置驱动**:监听 MCP 配置变更事件,自动同步知识库配置。 +3. **嵌入与向量检索**:集成本地嵌入模型与 DuckDB 向量数据库,支持高效检索。 +4. **事件与状态**:通过 eventBus 监控配置和知识库状态,发布相关事件。 + +## 核心组件 + +```mermaid +classDiagram + class KnowledgePresenter { + -configPresenter: IConfigPresenter + -storageDir: string + -storePresenterCache: Map + +create() + +reset() + +delete() + +addFile() + +deleteFile() + +reAddFile() + +queryFile() + +listFiles() + +similarityQuery() + +closeAll() + } + + class KnowledgeStorePresenter { + -vectorP: IVectorDatabasePresenter + -config: BuiltinKnowledgeConfig + +addFile() + +deleteFile() + +reAddFile() + +queryFile() + +listFiles() + +similarityQuery() + +reset() + +destroy() + +close() + } + + class IVectorDatabasePresenter { + <> + +initialize() + +open() + +close() + +insertFile() + +insertVectors() + +deleteVectorsByFile() + +deleteFile() + +queryFile() + +listFiles() + +similarityQuery() + +destroy() + } + + KnowledgePresenter o-- KnowledgeStorePresenter : manages + KnowledgeStorePresenter o-- IVectorDatabasePresenter +``` + +## 数据流 + +### 1. 配置变更与知识库同步 + +```mermaid +sequenceDiagram + participant eventBus + participant KnowledgePresenter + participant ConfigPresenter + + eventBus->>KnowledgePresenter: MCP_EVENTS.CONFIG_CHANGED + KnowledgePresenter->>ConfigPresenter: diffKnowledgeConfigs(configs) + KnowledgePresenter->>ConfigPresenter: setKnowledgeConfigs(configs) + KnowledgePresenter->>KnowledgePresenter: create/delete/reset等 +``` + +### 2. 文件入库与检索流程 + +```mermaid +sequenceDiagram + participant User + participant KnowledgePresenter + participant KnowledgeStorePresenter + participant IVectorDatabasePresenter + + User->>KnowledgePresenter: addFile(id, filePath) + KnowledgePresenter->>KnowledgeStorePresenter: addFile(filePath) + KnowledgeStorePresenter->>IVectorDatabasePresenter: insertFile/insertVectors + KnowledgeStorePresenter-->>KnowledgePresenter: 任务完成事件 + KnowledgePresenter-->>User: 文件入库结果 + + User->>KnowledgePresenter: similarityQuery(id, key) + KnowledgePresenter->>KnowledgeStorePresenter: similarityQuery(key) + KnowledgeStorePresenter->>IVectorDatabasePresenter: similarityQuery + KnowledgeStorePresenter-->>KnowledgePresenter: 检索结果 + KnowledgePresenter-->>User: 检索结果 +``` + +## 关键设计 + +1. **分层架构**:接口层(IKnowledgePresenter)、管理层(KnowledgePresenter)、业务层(KnowledgeStorePresenter)、存储层(IVectorDatabasePresenter)、配置层(ConfigPresenter)。 +2. **事件驱动**:通过 eventBus 监听 MCP 配置变更,自动同步知识库。 +3. **高性能本地检索**:集成 DuckDB 向量数据库和本地嵌入模型。 +4. **配置驱动与持久化**:所有知识库配置通过 ConfigPresenter 管理和持久化。 +5. **健壮性**:事件回调均有校验和异常处理,防止脏数据和异常中断。 diff --git a/docs/builtin-knowledge-design.md b/docs/builtin-knowledge-design.md new file mode 100644 index 000000000..a70df1930 --- /dev/null +++ b/docs/builtin-knowledge-design.md @@ -0,0 +1,221 @@ +# BuiltinKnowledge 设计文档 + +## 1. 核心类设计 + +### 1.1 KnowledgePresenter + +`KnowledgePresenter` (`src/main/presenter/knowledgePresenter/index.ts`) 是模块主入口,实现了 `IKnowledgePresenter` 接口,主要职责: + +- 依赖 `IConfigPresenter` 获取和管理知识库配置。 +- 监听 `MCP_EVENTS.CONFIG_CHANGED`,自动同步配置,按 diff 结果分别处理新增、删除、更新。 +- 管理 RAG 实例缓存,负责知识库的创建、重置、删除、文件增删查、相似度检索等。 +- 通过 eventBus 触发知识库相关事件。 + +**关键方法**: + +- `create(config)`: 创建知识库(初始化 RAG 实例)。 +- `reset(id)`: 重置知识库内容。 +- `delete(id)`: 删除知识库及本地存储。 +- `addFile(id, filePath)`: 添加文件到知识库。 +- `deleteFile(id, fileId)`: 删除知识库中的文件。 +- `reAddFile(id, fileId)`: 重新处理文件。 +- `queryFile(id, fileId)`, `listFiles(id)`: 查询/列出知识库文件。 +- `similarityQuery(id, key)`: 相似度检索。 +- `closeAll()`: 关闭所有 RAG 实例。 + +### 1.2 KnowledgeStorePresenter + +`KnowledgeStorePresenter` (`src/main/presenter/knowledgePresenter/KnowledgeStorePresenter.ts`) 负责知识库的核心业务逻辑: + +- 文件分块、嵌入生成、向量入库、相似度检索。 +- 依赖 `IVectorDatabasePresenter` 进行底层向量存储和检索。 +- 处理文件状态、异步任务和异常。 + +**关键方法**: + +- `addFile(filePath)`: 文件分块、嵌入、入库。 +- `deleteFile(fileId)`: 删除文件及其向量。 +- `reAddFile(fileId)`: 重新处理文件。 +- `queryFile(fileId)`, `listFiles()`: 查询/列出文件。 +- `similarityQuery(key)`: 相似度检索。 +- `reset()`, `destroy()`, `close()`: 管理生命周期。 + +### 1.3 IVectorDatabasePresenter + +`IVectorDatabasePresenter`(接口,DuckDBPresenter 实现)负责本地向量数据库操作: + +- 初始化、打开、关闭数据库。 +- 插入文件、插入/删除向量、文件查询、向量检索等。 + +### 1.4 ConfigPresenter + +`ConfigPresenter` 负责知识库配置的持久化、读取、diff(增删改对比)等能力。 + +- `getKnowledgeConfigs()`, `setKnowledgeConfigs()`, `diffKnowledgeConfigs()` 等。 + +## 2. 文件入库流程 + +```mermaid +sequenceDiagram + participant User + participant KnowledgePresenter + participant KnowledgeStorePresenter + participant IVectorDatabasePresenter + participant LLMProvider + participant EventBus + + User->>KnowledgePresenter: addFile(id, filePath) + KnowledgePresenter->>KnowledgeStorePresenter: addFile(filePath) + KnowledgeStorePresenter->>KnowledgeStorePresenter: 读取文件内容 + KnowledgeStorePresenter->>IVectorDatabasePresenter: insertFile(fileMessage) + KnowledgeStorePresenter->>KnowledgeStorePresenter: 分块 (splitText) + loop 对每个chunk + KnowledgeStorePresenter->>LLMProvider: getEmbeddings(chunk) + LLMProvider-->>KnowledgeStorePresenter: vector + end + KnowledgeStorePresenter->>IVectorDatabasePresenter: insertVectors(vectors) + KnowledgeStorePresenter->>IVectorDatabasePresenter: updateFile(status=completed) + KnowledgeStorePresenter->>EventBus: RAG_EVENTS.FILE_UPDATED (文件处理完成) + KnowledgeStorePresenter-->>KnowledgePresenter: fileTask Promise resolve + KnowledgePresenter-->>User: 返回文件入库结果 + Note over KnowledgeStorePresenter,IVectorDatabasePresenter: 异常时更新status=error并通知EventBus +``` + +## 3. 检索流程 + +```mermaid +sequenceDiagram + participant User + participant KnowledgePresenter + participant KnowledgeStorePresenter + participant LLMProvider + participant IVectorDatabasePresenter + + User->>KnowledgePresenter: similarityQuery(id, key) + KnowledgePresenter->>KnowledgeStorePresenter: similarityQuery(key) + KnowledgeStorePresenter->>LLMProvider: getEmbeddings([key]) + LLMProvider-->>KnowledgeStorePresenter: embedding + KnowledgeStorePresenter->>IVectorDatabasePresenter: similarityQuery(embedding) + IVectorDatabasePresenter-->>KnowledgeStorePresenter: 检索结果(相关片段、距离、元数据) + KnowledgeStorePresenter-->>KnowledgePresenter: 检索结果 + KnowledgePresenter-->>User: 检索结果 +``` + +## 4. 配置变更到知识库创建流程 + +当 MCP 配置变更(如新增/修改 builtinKnowledge 配置)时,系统自动同步并创建/更新本地知识库,流程如下: + +```mermaid +sequenceDiagram + participant MCPConfHelper + participant EventBus + participant KnowledgePresenter + participant ConfigPresenter + participant KnowledgeStorePresenter + participant IVectorDatabasePresenter (DuckDB) + + MCPConfHelper->>EventBus: send(MCP_EVENTS.CONFIG_CHANGED, { mcpServers, ... }) + EventBus->>KnowledgePresenter: MCP_EVENTS.CONFIG_CHANGED + KnowledgePresenter->>ConfigPresenter: diffKnowledgeConfigs(configs) + KnowledgePresenter->>ConfigPresenter: setKnowledgeConfigs(configs) + alt 新增/变更 + KnowledgePresenter->>KnowledgePresenter: create(config) + KnowledgePresenter->>KnowledgeStorePresenter: createKnowledgeStorePresenter(config) + KnowledgePresenter->>IVectorDatabasePresenter (DuckDB): getVectorDatabasePresenter(id, dimensions, normalized) + IVectorDatabasePresenter (DuckDB)-->>KnowledgePresenter: DuckDB 实例 + KnowledgePresenter->>KnowledgeStorePresenter: new KnowledgeStorePresenter(db, config) + end + alt 删除 + KnowledgePresenter->>KnowledgePresenter: delete(id) + KnowledgePresenter->>IVectorDatabasePresenter (DuckDB): 清理本地存储 + end +``` + +**说明:** +- MCPConfHelper 负责监听和发出配置变更事件。 +- KnowledgePresenter 监听事件,调用 ConfigPresenter 进行配置 diff 和持久化。 +- 对于新增/变更,KnowledgePresenter 创建 KnowledgeStorePresenter,并初始化/获取 DuckDB 实例。 +- 对于删除,KnowledgePresenter 清理本地存储和缓存。 + +## 5. 事件系统 + +BuiltinKnowledge 通过 eventBus 发出以下事件: + +| 事件名称 | 触发时机 | 触发源 | 参数 | +| ---------------------------------- | -------------------------------- | ----------------- | ------------------------- | +| `MCP_EVENTS.CONFIG_CHANGED` | 配置变更 | eventBus | configs | +| `RAG_EVENTS.FILE_UPDATED` | 文件处理完成/状态变更 | KnowledgePresenter | KnowledgeFileMessage | + +## 6. 配置管理 + +知识库相关配置通过 `ConfigPresenter` 管理,持久化存储。 + +**核心配置项**: + +- `knowledgeConfigs`: `BuiltinKnowledgeConfig[]` - 所有知识库实例配置。 + +**`BuiltinKnowledgeConfig` 接口**: + +```typescript +type BuiltinKnowledgeConfig = { + id: string + description: string + embedding: ModelProvider + dimensions: number + normalized: boolean + chunkSize?: number + chunkOverlap?: number + fragmentsNumber: number + enabled: boolean +} +``` + +## 7. 扩展指南 + +### 7.1 添加新向量数据库 + +1. 实现 `IVectorDatabasePresenter` 接口。 +2. 在 `KnowledgePresenter` 中根据配置选择不同数据库实现。 + +### 7.2 支持新嵌入模型 + +1. 扩展 `ModelProvider` 类型和相关调用逻辑。 +2. 在 `KnowledgeStorePresenter` 中适配新模型。 + +### 7.3 自定义事件与回调 + +1. 在 `KnowledgePresenter`/`KnowledgeStorePresenter` 中增加事件触发点。 +2. 在前端 UI 层监听并响应相关事件。 + +## 8. DuckDBPresenter 主要流程与细节 + +DuckDBPresenter 作为本地向量数据库的实现,负责表结构初始化、向量插入、检索、文件元数据管理等。下图展示其主要方法调用和数据流: + +```mermaid +flowchart TD + A[initialize] --> B[CreateTableAndIndex] + C[open] --> D[OpenDBConnection] + E[insertFile] --> F[InsertFileMetadata] + G[insertVectors] --> H[BatchInsertVectors] + I[updateFile] --> J[UpdateFileStatusOrMetadata] + K[deleteVectorsByFile] --> L[DeleteVectorsForFile] + M[deleteFile] --> N[DeleteFileMetadata] + O[similarityQuery] --> P[VectorANNQuery] + Q[queryFile] --> R[QueryFileMetadata] + S[listFiles] --> T[ListAllFiles] + U[destroyOrClose] --> V[CloseConnectionOrRelease] + + %% Main data flow + G --> P + E --> Q + E --> S + K --> M +``` + +**说明:** +- `initialize` 负责表结构和索引的创建,支持多维度和不同度量方式(如 cosine、ip)。 +- `insertFile`/`insertVectors` 分别管理文件元数据和向量数据,支持批量插入。 +- `similarityQuery` 实现高效 ANN 检索,返回相关片段及距离。 +- `deleteVectorsByFile`、`deleteFile` 支持文件级别的物理删除。 +- `updateFile`、`queryFile`、`listFiles` 支持文件状态和元数据的维护。 +- `destroy`/`close` 负责资源释放和连接关闭。 diff --git a/docs/builtin-knowledge.md b/docs/builtin-knowledge.md new file mode 100644 index 000000000..4fcc98148 --- /dev/null +++ b/docs/builtin-knowledge.md @@ -0,0 +1,99 @@ + +# BuiltinKnowledge(内置知识库)模块设计与架构文档 + + +## 1. 模块定位与作用 + +BuiltinKnowledge(内置知识库)是 DeepChat 内置的本地知识库服务,作为 MCP(Model Context Protocol)生态中的一类特殊服务器,主要用于本地知识的存储、管理与检索,支持 RAG(Retrieval-Augmented Generation)等场景。 + +- **本地化**:数据全部本地存储,无需外部依赖。 +- **高性能**:集成高效嵌入模型与 DuckDB 本地向量数据库,适合小/中型知识库。 +- **事件驱动**:通过 MCP 配置变更事件自动同步知识库配置。 + + +## 2. 架构与核心组件 + +### 2.1 主要类型与配置 + +- **BuiltinKnowledgeConfig**:知识库配置类型,包含 id、description、embedding(嵌入模型)、dimensions、normalized、chunkSize、chunkOverlap、fragmentsNumber、enabled 等字段。 +- **ModelProvider**:嵌入模型描述,含 providerId、modelId。 + +### 2.2 主要类与职责 + +- **KnowledgePresenter** + - 负责知识库的生命周期管理(创建、重置、删除、文件增删查等)。 + - 监听 MCP_EVENTS.CONFIG_CHANGED 事件,自动同步配置。 + - 依赖 ConfigPresenter 进行配置存取与差异对比。 + - 缓存并管理 RAG 实例。 + +- **RagPresenter** + - 封装文件分块、嵌入生成、向量入库、相似度检索等核心逻辑。 + - 底层依赖 DuckDBPresenter 作为本地向量数据库。 + +- **ConfigPresenter** + - 提供知识库配置的持久化、读取、diff(增删改对比)等能力。 + - 通过 diffKnowledgeConfigs、setKnowledgeConfigs 等方法管理配置。 + + +### 2.3 事件驱动同步机制 + +- 监听 `MCP_EVENTS.CONFIG_CHANGED`,自动同步 MCP 配置中的 builtinKnowledge。 +- 通过 `ConfigPresenter.diffKnowledgeConfigs` 对比新旧配置,分别处理新增、删除、更新。 +- 通过 `setKnowledgeConfigs` 持久化最新配置。 + + +## 3. 数据流与调用链 + +### 3.1 配置变更与同步 + +```mermaid +sequenceDiagram + participant eventBus + participant KnowledgePresenter + participant ConfigPresenter + + eventBus->>KnowledgePresenter: MCP_EVENTS.CONFIG_CHANGED + KnowledgePresenter->>ConfigPresenter: diffKnowledgeConfigs(configs) + KnowledgePresenter->>ConfigPresenter: setKnowledgeConfigs(configs) + KnowledgePresenter->>KnowledgePresenter: create/delete/重置等 +``` + +### 3.2 典型操作链 + +- **创建知识库**:KnowledgePresenter.create(config) → createRagPresenter(config) → new RagPresenter(db, config) +- **重置知识库**:KnowledgePresenter.reset(id) → RagPresenter.reset() +- **删除知识库**:KnowledgePresenter.delete(id) +- **文件入库**:addFile(id, filePath) → RagPresenter.addFile(filePath) → 分块+嵌入+向量入库 +- **相似度检索**:similarityQuery(id, key) → RagPresenter.similarityQuery(key) + + +## 4. 健壮性与事件机制 + +- 事件回调对 payload 结构进行校验,防止脏数据导致异常。 +- 事件处理均有 try-catch,提升健壮性。 + + +## 5. 配置结构 + +- MCP 配置中的 builtinKnowledge 结构: + - `env.configs: BuiltinKnowledgeConfig[]`,每个元素描述一个知识库实例。 +- 本地持久化通过 ConfigPresenter 管理。 + + +## 6. 扩展性与最佳实践 + +- 支持多知识库并行管理,底层 DuckDB 支持多实例。 +- 嵌入模型和向量库可扩展。 +- 推荐与 MCP 生态其他知识库统一管理和 UI 入口。 + + +## 7. 典型场景 + +- 本地知识问答、FAQ、文档检索。 +- 结合 LLM 进行 RAG 增强。 +- 离线场景下的知识管理。 + + +--- + +如需详细 API 或配置字段说明,请参考 `KnowledgePresenter`、`BuiltinKnowledgeConfig` 相关源码与类型定义。 diff --git a/package.json b/package.json index f695851b7..7b68b7b71 100644 --- a/package.json +++ b/package.json @@ -54,16 +54,20 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.53.0", + "@duckdb/node-api": "1.3.2-alpha.24", "@e2b/code-interpreter": "^1.5.1", "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@google/genai": "^1.5.1", + "@langchain/core": "^0.3.62", + "@langchain/textsplitters": "^0.1.0", "@jxa/run": "^1.4.0", "@modelcontextprotocol/sdk": "^1.13.1", "axios": "^1.7.9", "better-sqlite3-multiple-ciphers": "11.10.0", "cheerio": "^1.0.0", "compare-versions": "^6.1.1", + "dayjs": "^1.11.13", "diff": "^7.0.0", "electron-log": "^5.3.3", "electron-store": "^8.2.0", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index abe370e78..c7142a6a8 100755 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1 +1,45 @@ +// install duckdb extension +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +async function installVssExtension() { + const __filename = fileURLToPath(import.meta.url) + const __dirname = path.dirname(__filename) + + try { + const duckdb = await import('@duckdb/node-api') + const inst = await duckdb.DuckDBInstance.create(':memory:') + const conn = await inst.connect() + + await conn.run('INSTALL vss') + const reader = await conn.runAndReadAll( + "SELECT install_path FROM duckdb_extensions() WHERE extension_name = 'vss'" + ) + const rows = reader.getRows() + if (rows.length === 0) { + throw new Error('VSS extension not found after installation') + } + const sourcePath = rows[0][0] + if (!sourcePath || typeof sourcePath !== 'string') { + throw new Error('Invalid extension path returned from DuckDB') + } + console.log('vss extension path:', sourcePath) + + const targetDir = path.join(__dirname, '../runtime/duckdb/extensions') + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + + const filename = sourcePath.substring(sourcePath.lastIndexOf(path.sep) + 1) + const targetPath = path.join(targetDir, filename) + fs.copyFileSync(sourcePath, targetPath) + console.log('Install duckdb extension successfully.') + } catch (error) { + console.error('Failed to install DuckDB extension:', error.message) + process.exit(1) + } +} + console.log('postinstall') +installVssExtension() diff --git a/src/main/events.ts b/src/main/events.ts index 46fe9db1d..c55a66c70 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -172,3 +172,8 @@ export const FLOATING_BUTTON_EVENTS = { POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变 ENABLED_CHANGED: 'floating-button:enabled-changed' // 悬浮按钮启用状态改变 } + +// 知识库事件 +export const RAG_EVENTS = { + FILE_UPDATED: 'rag:file-updated' // 文件状态更新 +} \ No newline at end of file diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 0c2084273..73154c58f 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -7,7 +7,8 @@ import { RENDERER_MODEL_META, MCPServerConfig, Prompt, - IModelConfig + IModelConfig, + BuiltinKnowledgeConfig } from '@shared/presenter' import { SearchEngineTemplate } from '@shared/chat' import { ModelType } from '@shared/model' @@ -22,6 +23,7 @@ import { presenter } from '@/presenter' import { compare } from 'compare-versions' import { defaultShortcutKey, ShortcutKeySetting } from './shortcutKeySettings' import { ModelConfigHelper } from './modelConfig' +import { KnowledgeConfHelper } from './knowledgeConfHelper' // 定义应用设置的接口 interface IAppSettings { @@ -79,6 +81,7 @@ export class ConfigPresenter implements IConfigPresenter { private currentAppVersion: string private mcpConfHelper: McpConfHelper // 使用MCP配置助手 private modelConfigHelper: ModelConfigHelper // 模型配置助手 + private knowledgeConfHelper: KnowledgeConfHelper // 知识配置助手 constructor() { this.userDataPath = app.getPath('userData') @@ -124,6 +127,9 @@ export class ConfigPresenter implements IConfigPresenter { // 初始化模型配置助手 this.modelConfigHelper = new ModelConfigHelper() + // 初始化知识配置助手 + this.knowledgeConfHelper = new KnowledgeConfHelper() + // 初始化provider models目录 this.initProviderModelsDir() @@ -1046,6 +1052,24 @@ export class ConfigPresenter implements IConfigPresenter { resetShortcutKeys() { this.setSetting('shortcutKey', { ...defaultShortcutKey }) } + + // 获取知识库配置 + getKnowledgeConfigs(): BuiltinKnowledgeConfig[] { + return this.knowledgeConfHelper.getKnowledgeConfigs() + } + + // 设置知识库配置 + setKnowledgeConfigs(configs: BuiltinKnowledgeConfig[]): void { + this.knowledgeConfHelper.setKnowledgeConfigs(configs) + } + + // 对比知识库配置差异 + diffKnowledgeConfigs(newConfigs: BuiltinKnowledgeConfig[]) { + return KnowledgeConfHelper.diffKnowledgeConfigs( + this.knowledgeConfHelper.getKnowledgeConfigs(), + newConfigs + ) + } } export { defaultShortcutKey } from './shortcutKeySettings' diff --git a/src/main/presenter/configPresenter/knowledgeConfHelper.ts b/src/main/presenter/configPresenter/knowledgeConfHelper.ts new file mode 100644 index 000000000..7a8cce23e --- /dev/null +++ b/src/main/presenter/configPresenter/knowledgeConfHelper.ts @@ -0,0 +1,51 @@ +import ElectronStore from 'electron-store' +import { BuiltinKnowledgeConfig } from '@shared/presenter' + +export class KnowledgeConfHelper { + private store: ElectronStore<{ knowledgeConfigs: BuiltinKnowledgeConfig[] }> + + constructor() { + this.store = new ElectronStore<{ knowledgeConfigs: BuiltinKnowledgeConfig[] }>({ + name: 'knowledge-configs', + defaults: { + knowledgeConfigs: [] + } + }) + } + + // 获取所有知识库配置 + getKnowledgeConfigs(): BuiltinKnowledgeConfig[] { + return this.store.get('knowledgeConfigs') || [] + } + + // 设置所有知识库配置 + setKnowledgeConfigs(configs: BuiltinKnowledgeConfig[]): void { + this.store.set('knowledgeConfigs', configs) + } + + /** + * diff 新旧配置,返回 { added, updated, deleted } + * @param oldConfigs + * @param newConfigs + * @returns + */ + static diffKnowledgeConfigs( + oldConfigs: BuiltinKnowledgeConfig[], + newConfigs: BuiltinKnowledgeConfig[] + ): { + added: BuiltinKnowledgeConfig[] + deleted: BuiltinKnowledgeConfig[] + updated: BuiltinKnowledgeConfig[] + } { + const oldMap = new Map(oldConfigs.map((cfg) => [cfg.id, cfg])) + const newMap = new Map(newConfigs.map((cfg) => [cfg.id, cfg])) + + const added = newConfigs.filter((cfg) => !oldMap.has(cfg.id)) + const deleted = oldConfigs.filter((cfg) => !newMap.has(cfg.id)) + const updated = newConfigs.filter( + (cfg) => oldMap.has(cfg.id) && JSON.stringify(cfg) !== JSON.stringify(oldMap.get(cfg.id)) + ) + + return { added, deleted, updated } + } +} diff --git a/src/main/presenter/configPresenter/mcpConfHelper.ts b/src/main/presenter/configPresenter/mcpConfHelper.ts index 5af29e817..d56ac1834 100644 --- a/src/main/presenter/configPresenter/mcpConfHelper.ts +++ b/src/main/presenter/configPresenter/mcpConfHelper.ts @@ -196,6 +196,18 @@ const DEFAULT_INMEMORY_SERVERS: Record = { }, disable: false }, + builtinKnowledge: { + args: [], + descriptions: 'DeepChat内置知识库检索服务', + icons: '📚', + autoApprove: ['all'], + type: 'inmemory' as MCPServerType, + command: 'builtinKnowledge', + env: { + configs: [] + }, + disable: false + }, 'deepchat-inmemory/deep-research-server': { args: [], descriptions: diff --git a/src/main/presenter/configPresenter/modelDefaultSettings.ts b/src/main/presenter/configPresenter/modelDefaultSettings.ts index 268a5ec32..e74000cef 100644 --- a/src/main/presenter/configPresenter/modelDefaultSettings.ts +++ b/src/main/presenter/configPresenter/modelDefaultSettings.ts @@ -1673,20 +1673,32 @@ export const defaultModelsSettings: DefaultModelSetting[] = [ id: 'text-embedding-nomic-embed-text', name: 'Nomic Embed Text', temperature: 0.0, - maxTokens: 8192, - contextLength: 8192, + maxTokens: 2048, + contextLength: 2048, match: ['text-embedding-nomic-embed-text', 'nomic/embed-text', 'nomic-embed-text'], vision: false, functionCall: false, reasoning: false, type: ModelType.Embedding }, + { + id: 'all-minilm', + name: 'all-minilm', + temperature: 0.0, + maxTokens: 512, + contextLength: 512, + match: ['all-minilm'], + vision: false, + functionCall: false, + reasoning: false, + type: ModelType.Embedding + }, { id: 'embedding', name: 'embedding', temperature: 0.0, - maxTokens: 4096, - contextLength: 4096, + maxTokens: 1024, + contextLength: 1024, match: ['embedding', 'embed'], vision: false, functionCall: false, diff --git a/src/main/presenter/filePresenter/BaseFileAdapter.ts b/src/main/presenter/filePresenter/BaseFileAdapter.ts index 977f20c66..27b015ab0 100644 --- a/src/main/presenter/filePresenter/BaseFileAdapter.ts +++ b/src/main/presenter/filePresenter/BaseFileAdapter.ts @@ -68,7 +68,7 @@ export abstract class BaseFileAdapter { } protected abstract getFileDescription(): string | undefined - protected abstract getContent(): Promise + public abstract getContent(): Promise public abstract getLLMContent(): Promise public abstract getThumbnail(): Promise } diff --git a/src/main/presenter/filePresenter/FilePresenter.ts b/src/main/presenter/filePresenter/FilePresenter.ts index eee57cafd..bee75efe0 100644 --- a/src/main/presenter/filePresenter/FilePresenter.ts +++ b/src/main/presenter/filePresenter/FilePresenter.ts @@ -92,14 +92,32 @@ export class FilePresenter implements IFilePresenter { } } - async prepareFile(absPath: string, typeInfo?: string): Promise { + /** + * 准备文件,返回一个完整的 MessageFile 对象,支持不同的 contentType(兼容旧方法调用) + * @param absPath + * @param typeInfo + * @param contentType + * @returns + */ + async prepareFileCompletely(absPath: string, typeInfo?: string, contentType?: null | 'origin' | 'llm-friendly'): Promise { const fullPath = path.join(absPath) try { const adapter = await this.createFileAdapter(fullPath, typeInfo) console.log('adapter', adapter) if (adapter) { await adapter.processFile() - const content = (await adapter.getLLMContent()) ?? '' + let content + switch (contentType) { + case 'llm-friendly': + content = await adapter.getLLMContent() + break + case 'origin': + content = await adapter.getContent() + break + default: + content = null + break + } const thumbnail = adapter.getThumbnail ? await adapter.getThumbnail() : undefined const result = { name: adapter.fileMetaData?.fileName ?? '', @@ -132,6 +150,10 @@ export class FilePresenter implements IFilePresenter { } } + async prepareFile(absPath: string, typeInfo?: string): Promise { + return this.prepareFileCompletely(absPath, typeInfo, 'llm-friendly') + } + private findAdapterForMimeType( mimeType: string, adapterMap: Map diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 92002695a..a49856654 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -21,6 +21,7 @@ import { TrayPresenter } from './trayPresenter' import { OAuthPresenter } from './oauthPresenter' import { FloatingButtonPresenter } from './floatingButtonPresenter' import { CONFIG_EVENTS, WINDOW_EVENTS } from '@/events' +import { KnowledgePresenter } from './knowledgePresenter' // IPC调用上下文接口 interface IPCCallContext { @@ -54,6 +55,7 @@ export class Presenter implements IPresenter { trayPresenter: TrayPresenter oauthPresenter: OAuthPresenter floatingButtonPresenter: FloatingButtonPresenter + knowledgePresenter: KnowledgePresenter // llamaCppPresenter: LlamaCppPresenter // 保留原始注释 constructor() { @@ -82,6 +84,7 @@ export class Presenter implements IPresenter { this.oauthPresenter = new OAuthPresenter() this.trayPresenter = new TrayPresenter() this.floatingButtonPresenter = new FloatingButtonPresenter(this.configPresenter) + this.knowledgePresenter = new KnowledgePresenter(this.configPresenter, dbDir) // this.llamaCppPresenter = new LlamaCppPresenter() // 保留原始注释 this.setupEventBus() // 设置事件总线监听 @@ -169,6 +172,7 @@ export class Presenter implements IPresenter { this.shortcutPresenter.destroy() // 销毁快捷键监听 this.syncPresenter.destroy() // 销毁同步相关资源 this.notificationPresenter.clearAllNotifications() // 清除所有通知 + this.knowledgePresenter.destroy() // 释放所有数据库连接 // 注意: trayPresenter.destroy() 在 main/index.ts 的 will-quit 事件中处理 // 此处不销毁 trayPresenter,其生命周期由 main/index.ts 管理 } diff --git a/src/main/presenter/knowledgePresenter/database/duckdbPresenter.ts b/src/main/presenter/knowledgePresenter/database/duckdbPresenter.ts new file mode 100644 index 000000000..e3aa33989 --- /dev/null +++ b/src/main/presenter/knowledgePresenter/database/duckdbPresenter.ts @@ -0,0 +1,344 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { DuckDBConnection, DuckDBInstance, arrayValue } from '@duckdb/node-api' +import { + IndexOptions, + InsertOptions, + QueryOptions, + QueryResult, + IVectorDatabasePresenter, + KnowledgeFileMessage +} from '@shared/presenter' + +import { nanoid } from 'nanoid' +import { app } from 'electron' + +const runtimeBasePath = path + .join(app.getAppPath(), 'runtime') + .replace('app.asar', 'app.asar.unpacked') +const extensionPath = path.join(runtimeBasePath, 'duckdb', 'extensions', 'vss.duckdb_extension') + +export class DuckDBPresenter implements IVectorDatabasePresenter { + private dbInstance!: DuckDBInstance + private connection!: DuckDBConnection + + private readonly dbPath: string + + private readonly vectorTable = 'vector' + private readonly fileTable = 'file' + + constructor(dbPath: string) { + this.dbPath = dbPath + } + + async initialize(dimensions: number, opts?: IndexOptions): Promise { + console.log(`[DuckDB] Initializing DuckDB database at ${this.dbPath}`) + if (fs.existsSync(this.dbPath)) { + console.error(`[DuckDB] Database ${this.dbPath} already exists`) + throw new Error('Database already exists, cannot initialize again.') + } + console.log(`[DuckDB] connect to db`) + await this.connect() + console.log(`[DuckDB] load vss extension`) + await this.installAndLoadVSS() + console.log(`[DuckDB] create vector table`) + await this.initVectorTable(dimensions) + console.log(`[DuckDB] create file table`) + await this.initFileTable() + console.log(`[DuckDB] create index`) + await this.initIndex(opts) + } + + async open(): Promise { + if (!fs.existsSync(this.dbPath)) { + console.error(`[DuckDB] Database ${this.dbPath} does not exist`) + throw new Error('Database does not exist, please initialize first.') + } + await this.connect() + await this.connection.run('CHECKPOINT;') + await this.installAndLoadVSS() + } + + async close(): Promise { + try { + if (this.connection) { + this.connection.closeSync() + } + if (this.dbInstance) { + this.dbInstance.closeSync() + } + console.log('[DuckDB] DuckDB connection closed') + } catch (err) { + console.error('[DuckDB] close error', err) + } + } + + /** + * 统一安全 SQL 执行,自动捕获异常并输出日志 + */ + private async safeRun(sql: string, params?: any[]): Promise { + try { + if (!this.connection) await this.connect() + if (params) { + return await this.connection.run(sql, params) + } else { + return await this.connection.run(sql) + } + } catch (err) { + console.error('[DuckDB] sql error', sql, params, err) + throw err + } + } + + private async connect() { + this.dbInstance = await DuckDBInstance.create(this.dbPath) + this.connection = await this.dbInstance.connect() + console.log(`[DuckDB] Connected to DuckDB at ${this.dbPath}`) + } + + /** 安装并加载 VSS 扩展 */ + private async installAndLoadVSS(): Promise { + if (fs.existsSync(extensionPath)) { + const escapedPath = extensionPath.replace(/\\/g, '\\\\') + console.log(`[DuckDB] LOAD VSS extension from ${escapedPath}`) + await this.safeRun(`LOAD '${escapedPath}';`) + } else { + console.log('[DuckDB] LOAD VSS extension online') + await this.safeRun(`INSTALL vss;`) + await this.safeRun(`LOAD vss;`) + } + await this.safeRun(`SET hnsw_enable_experimental_persistence = true;`) + } + + /** 创建定长向量表 */ + private async initVectorTable(dimensions: number): Promise { + await this.safeRun( + `CREATE TABLE IF NOT EXISTS ${this.vectorTable} ( + id VARCHAR PRIMARY KEY, + embedding FLOAT[${dimensions}], + metadata JSON, + file_id VARCHAR + );` + ) + } + + /** 创建文件元数据表 */ + private async initFileTable(): Promise { + await this.safeRun( + `CREATE TABLE IF NOT EXISTS ${this.fileTable} ( + id VARCHAR PRIMARY KEY, + name VARCHAR, + path VARCHAR, + mime_type VARCHAR, + status VARCHAR, + uploaded_at BIGINT, + metadata JSON + );` + ) + } + + /** 创建索引 */ + private async initIndex(opts?: IndexOptions): Promise { + const metric = opts?.metric || 'cosine' // 支持 'l2sq' | 'cosine' | 'ip' + const M = opts?.M || 16 + const efConstruction = opts?.efConstruction || 200 + const sql = `CREATE INDEX IF NOT EXISTS idx_${this.vectorTable}_emb + ON ${this.vectorTable} + USING HNSW (embedding) + WITH ( + metric='${metric}', + M=${M}, + ef_construction=${efConstruction} + );` + await this.safeRun(sql) + await this.safeRun( + `CREATE INDEX IF NOT EXISTS idx_${this.vectorTable}_file_id ON ${this.vectorTable} (file_id);` + ) + } + + async insertVector(opts: InsertOptions): Promise { + const id = nanoid() + const vec = arrayValue(Array.from(opts.vector)) + const meta = opts.metadata ? JSON.stringify(opts.metadata) : null + const fileId = opts.fileId + await this.safeRun( + `INSERT INTO ${this.vectorTable} (id, embedding, metadata, file_id) + VALUES (?, ?::FLOAT[], ?::JSON, ?);`, + [id, vec, meta, fileId] + ) + } + + async insertVectors(records: InsertOptions[]): Promise { + if (!records.length) return + // 构造批量插入 SQL + const valuesSql = records.map(() => '(?, ?::FLOAT[], ?::JSON, ?)').join(', ') + const sql = `INSERT INTO ${this.vectorTable} (id, embedding, metadata, file_id) VALUES ${valuesSql};` + const params: any[] = [] + for (const r of records) { + params.push(nanoid()) + params.push(arrayValue(Array.from(r.vector))) + params.push(r.metadata ? JSON.stringify(r.metadata) : null) + params.push(r.fileId) + } + await this.safeRun(sql, params) + } + + async similarityQuery(vector: number[], options: QueryOptions): Promise { + const k = options.topK + const fn = + options.metric === 'ip' + ? 'array_negative_inner_product' + : options.metric === 'cosine' + ? 'array_cosine_distance' + : 'array_distance' + const where = options.threshold != null ? `WHERE ${fn}(embedding, ?) <= ?` : '' + const sql = ` + SELECT id, metadata, ${fn}(embedding, ?) AS distance + FROM ${this.vectorTable} + ${where} + ORDER BY distance + LIMIT ?; + ` + const embParam = arrayValue(Array.from(vector)) + const paramsArr: any[] = [embParam] + if (options.threshold != null) { + paramsArr.push(options.threshold) + } + paramsArr.push(k) + try { + const reader = await this.connection.runAndReadAll(sql, paramsArr) + const rows = reader.getRowObjectsJson() + return rows.map((r: any) => ({ + id: r.id, + metadata: typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata, + distance: r.distance + })) + } catch (err) { + console.error('[DuckDB] similarityQuery error', sql, paramsArr, err) + throw err + } + } + + async deleteVector(id: string): Promise { + await this.safeRun(`DELETE FROM ${this.vectorTable} WHERE id = ?;`, [id]) + } + + async deleteVectorsByFile(fileId: string): Promise { + await this.safeRun(`DELETE FROM ${this.vectorTable} WHERE file_id = ?;`, [fileId]) + } + + async insertFile(file: KnowledgeFileMessage): Promise { + await this.safeRun( + `INSERT INTO ${this.fileTable} (id, name, path, mime_type, status, uploaded_at, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?::JSON);`, + [ + file.id, + file.name, + file.path, + file.mimeType, + file.status, + String(file.uploadedAt), + file.metadata ? JSON.stringify(file.metadata) : null + ] + ) + } + + async updateFile(file: KnowledgeFileMessage): Promise { + await this.safeRun( + `UPDATE ${this.fileTable} SET name = ?, path = ?, mime_type = ?, status = ?, uploaded_at = ?, metadata = ?::JSON + WHERE id = ?;`, + [ + file.name, + file.path, + file.mimeType, + file.status, + String(file.uploadedAt), + file.metadata ? JSON.stringify(file.metadata) : null, + file.id + ] + ) + } + + async queryFile(id: string): Promise { + const sql = `SELECT * FROM ${this.fileTable} WHERE id = ?;` + try { + const reader = await this.connection.runAndReadAll(sql, [id]) + const rows = reader.getRowObjectsJson() + if (rows.length === 0) return null + const row = rows[0] + return this.toKnowledgeFileMessage(row) + } catch (err) { + console.error('[DuckDB] queryFile error', sql, id, err) + throw err + } + } + + async listFiles(): Promise { + const sql = `SELECT * FROM ${this.fileTable} ORDER BY uploaded_at DESC;` + try { + const reader = await this.connection.runAndReadAll(sql) + const rows = reader.getRowObjectsJson() + return rows.map((row) => this.toKnowledgeFileMessage(row)) + } catch (err) { + console.error('[DuckDB] listFiles error', sql, err) + throw err + } + } + + async queryFiles(where: Partial): Promise { + const camelToSnake = (key: string) => + key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) + + const entries = Object.entries(where).filter(([, value]) => value !== undefined) + + let sql = `SELECT * FROM ${this.fileTable}` + const params: any[] = [] + + if (entries.length > 0) { + const conditions = entries.map(([key]) => `${camelToSnake(key)} = ?`).join(' AND ') + sql += ` WHERE ${conditions}` + params.push(...entries.map(([, value]) => value)) + } + + sql += ` ORDER BY uploaded_at DESC;` + + try { + const reader = await this.connection.runAndReadAll(sql, params) + const rows = reader.getRowObjectsJson() + return rows.map((row) => this.toKnowledgeFileMessage(row)) + } catch (err) { + console.error('[DuckDB] queryFiles error', sql, params, err) + throw err + } + } + + private toKnowledgeFileMessage(o: any): KnowledgeFileMessage { + return { + id: o.id, + name: o.name, + path: o.path, + mimeType: o.mime_type, + status: o.status, + uploadedAt: Number(o.uploaded_at), + metadata: typeof o.metadata === 'string' ? JSON.parse(o.metadata) : o.metadata + } + } + + async deleteFile(id: string): Promise { + await this.safeRun(`DELETE FROM ${this.fileTable} WHERE id = ?;`, [id]) + } + + async destroy(): Promise { + await this.close() + // 删除数据库文件 + try { + if (fs.existsSync(this.dbPath)) { + fs.rmSync(this.dbPath, { recursive: true }) + console.log(`[DuckDB] Database at ${this.dbPath} destroyed.`) + } + } catch (err) { + console.error(`[DuckDB] Error destroying database at ${this.dbPath}:`, err) + } + } +} diff --git a/src/main/presenter/knowledgePresenter/index.ts b/src/main/presenter/knowledgePresenter/index.ts new file mode 100644 index 000000000..1bac94565 --- /dev/null +++ b/src/main/presenter/knowledgePresenter/index.ts @@ -0,0 +1,268 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { + IConfigPresenter, + IKnowledgePresenter, + BuiltinKnowledgeConfig, + MCPServerConfig, + KnowledgeFileMessage, + QueryResult, + KnowledgeFileResult +} from '@shared/presenter' +import { eventBus, SendTarget } from '@/eventbus' +import { MCP_EVENTS, RAG_EVENTS } from '@/events' +import { DuckDBPresenter } from './database/duckdbPresenter' +import { KnowledgeStorePresenter } from './knowledgeStorePresenter' + +export class KnowledgePresenter implements IKnowledgePresenter { + /** + * 知识库存储目录 + */ + private readonly storageDir + + private readonly configP: IConfigPresenter + + constructor(configP: IConfigPresenter, dbDir: string) { + console.log('[RAG] Initializing Built-in Knowledge Presenter') + this.configP = configP + this.storageDir = path.join(dbDir, 'KnowledgeBase') + + this.initStorageDir() + this.recoverUnfinishedTasks() + this.setupEventBus() + } + + /** + * 初始化知识库存储目录 + */ + private initStorageDir = (): void => { + if (!fs.existsSync(this.storageDir)) { + fs.mkdirSync(this.storageDir, { recursive: true }) + } + } + + private setupEventBus = (): void => { + // 监听知识库相关事件 + eventBus.on(MCP_EVENTS.CONFIG_CHANGED, async (payload) => { + try { + if ( + !payload || + typeof payload !== 'object' || + !payload.mcpServers || + typeof payload.mcpServers !== 'object' + ) { + console.warn('[RAG] Invalid payload for CONFIG_CHANGED event:', payload) + return + } + const mcpServers = payload.mcpServers + const builtinConfig = mcpServers['builtinKnowledge'] as MCPServerConfig + if (builtinConfig?.env && Array.isArray(builtinConfig.env.configs)) { + const configs = builtinConfig.env.configs as BuiltinKnowledgeConfig[] + console.log('[RAG] Received builtinKnowledge config update:', configs) + const diffs = this.configP.diffKnowledgeConfigs(configs) + this.configP.setKnowledgeConfigs(configs) + // 处理新增、删除和更新的配置 + if (diffs.deleted.length > 0) { + diffs.deleted.forEach((config) => this.delete(config.id)) + } + if (diffs.added.length > 0) { + diffs.added.forEach((config) => { + console.log(`[RAG] New knowledge config added: ${config.id}`) + this.create(config) + }) + } + if (diffs.updated.length > 0) { + } + console.log('[RAG] Updated knowledge configs:', configs) + } else { + console.warn('[RAG] builtinKnowledge config missing or invalid:', builtinConfig) + } + } catch (err) { + console.error('[RAG] Error handling CONFIG_CHANGED event:', err) + } + }) + } + + /** + * 创建知识库(初始化 RAG 应用) + */ + create = async (config: BuiltinKnowledgeConfig): Promise => { + this.createStorePresenter(config) + } + + /** + * 删除知识库(移除本地存储) + */ + delete = async (id: string): Promise => { + if (this.storePresenterCache.has(id)) { + const rag = this.storePresenterCache.get(id) as KnowledgeStorePresenter + await rag.destroy() + } else { + const dbPath = path.join(this.storageDir, id) + if (fs.existsSync(dbPath)) { + fs.rmSync(dbPath, { recursive: true }) + } + } + } + + /** + * 缓存 RAG 应用实例 + */ + private storePresenterCache: Map = new Map() + + /** + * 创建 RAG 应用实例 + * @param params BuiltinKnowledgeConfig + * @returns KnowledgeStorePresenter + */ + private createStorePresenter = async (config: BuiltinKnowledgeConfig): Promise => { + let rag: KnowledgeStorePresenter + const db = await this.getVectorDatabasePresenter( + config.id, + config.dimensions, + config.normalized + ) + try { + rag = new KnowledgeStorePresenter(db, config) + } catch (e) { + throw new Error(`Failed to create storePresenter: ${e}`) + } + + this.storePresenterCache.set(config.id, rag) + return rag + } + + /** + * 获取 RAG 应用实例 + * @param id 知识库 ID + */ + private getStorePresenter = async (id: string): Promise => { + // 缓存命中直接返回 + if (this.storePresenterCache.has(id)) { + return this.storePresenterCache.get(id) as KnowledgeStorePresenter + } + // 获取配置 + const configs = this.configP.getKnowledgeConfigs() + const config = configs.find((cfg) => cfg.id === id) + if (!config) { + throw new Error(`Knowledge config not found for id: ${id}`) + } + // DuckDB 存储 + const db = await this.getVectorDatabasePresenter(id, config.dimensions, config.normalized) + // 创建 RAG 应用实例 + const rag = new KnowledgeStorePresenter(db, config) + this.storePresenterCache.set(id, rag) + return rag + } + + /** + * 获取向量数据库实例 + * @param id 知识库 ID + * @param dimensions 向量维度 + * @returns + */ + private getVectorDatabasePresenter = async ( + id: string, + dimensions: number, + normalized: boolean + ) => { + const dbPath = path.join(this.storageDir, id) + if (fs.existsSync(dbPath)) { + const db = new DuckDBPresenter(dbPath) + await db.open() + return db + } + // 如果数据库不存在,则初始化 + const db = new DuckDBPresenter(dbPath) + await db.initialize(dimensions, { + metric: normalized ? 'cosine' : 'ip' + }) + return db + } + + private async handleFileTask( + id: string, + fileHandler: (rag: KnowledgeStorePresenter) => Promise<{ data: KnowledgeFileMessage; task: Promise }>, + errorMsg: string + ): Promise { + try { + const rag = await this.getStorePresenter(id) + const fileTask = await fileHandler(rag) + fileTask.task + .then((message) => { + eventBus.sendToRenderer(RAG_EVENTS.FILE_UPDATED, SendTarget.ALL_WINDOWS, message) + }) + .catch((err) => { + // 可选:记录异步任务异常 + console.error(`${errorMsg}异步任务失败:`, err) + }) + return { + data: fileTask.data + } + } catch (err) { + return { + error: `${errorMsg}: ${err instanceof Error ? err.message : String(err)}` + } + } + } + + async addFile(id: string, filePath: string): Promise { + return this.handleFileTask( + id, + (rag) => rag.addFile(filePath), + '添加文件失败' + ) + } + + async deleteFile(id: string, fileId: string): Promise { + const rag = await this.getStorePresenter(id) + await rag.deleteFile(fileId) + } + + async reAddFile(id: string, fileId: string): Promise { + return this.handleFileTask( + id, + (rag) => rag.reAddFile(fileId), + '重新添加文件失败' + ) + } + + async queryFile(id: string, fileId: string): Promise { + const rag = await this.getStorePresenter(id) + return await rag.queryFile(fileId) + } + + async listFiles(id: string): Promise { + const rag = await this.getStorePresenter(id) + return await rag.listFiles() + } + + async closeAll(): Promise { + this.storePresenterCache.forEach((rag) => { + rag.close() + }) + } + + async destroy(): Promise { + this.closeAll() + } + + async similarityQuery(id: string, key: string): Promise { + const rag = await this.getStorePresenter(id) + return await rag.similarityQuery(key) + } + + private async recoverUnfinishedTasks(): Promise { + console.log('[RAG] Recovering unfinished tasks...') + const configs = this.configP.getKnowledgeConfigs() + for (const config of configs) { + try { + const storePresenter = await this.getStorePresenter(config.id) + await storePresenter.recoverProcessingFiles() + } catch (err) { + console.error(`[RAG] Error recovering tasks for knowledge base ${config.id}:`, err) + } + } + } +} diff --git a/src/main/presenter/knowledgePresenter/knowledgeStorePresenter.ts b/src/main/presenter/knowledgePresenter/knowledgeStorePresenter.ts new file mode 100644 index 000000000..1793b3d0b --- /dev/null +++ b/src/main/presenter/knowledgePresenter/knowledgeStorePresenter.ts @@ -0,0 +1,198 @@ +import fs from 'node:fs' + +import { + BuiltinKnowledgeConfig, + IVectorDatabasePresenter, + KnowledgeFileMessage, + QueryResult +} from '@shared/presenter' +import { presenter } from '@/presenter' +import { nanoid } from 'nanoid' +import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters' +import { sanitizeText } from '@/utils/strings' + +export class KnowledgeStorePresenter { + private readonly vectorP: IVectorDatabasePresenter + private readonly config: BuiltinKnowledgeConfig + + constructor(vectorP: IVectorDatabasePresenter, config: BuiltinKnowledgeConfig) { + this.vectorP = vectorP + this.config = config + } + + async addFile( + filePath: string, + fileId?: string + ): Promise<{ data: KnowledgeFileMessage; task: Promise }> { + if (fs.existsSync(filePath) === false) { + throw new Error('文件不存在,请检查路径是否正确') + } + + const mimeType = await presenter.filePresenter.getMimeType(filePath) + const fileInfo = await presenter.filePresenter.prepareFileCompletely( + filePath, + mimeType, + 'origin' + ) + + const fileMessage = { + id: fileId ?? nanoid(), + name: fileInfo.name, + path: fileInfo.path, + mimeType, + status: 'processing', + uploadedAt: new Date().getTime(), + metadata: { + size: fileInfo.metadata.fileSize + } + } as KnowledgeFileMessage + fileId ? await this.vectorP.updateFile(fileMessage) : await this.vectorP.insertFile(fileMessage) + + const fileTask = this.fileTask(fileInfo.content, fileMessage) + + return { data: fileMessage, task: fileTask } + } + + private async fileTask( + content: string, + fileMessage: KnowledgeFileMessage + ): Promise { + try { + // 1. 分片 + const chunker = new RecursiveCharacterTextSplitter({ + chunkSize: this.config.chunkSize, + chunkOverlap: this.config.chunkOverlap + }) + const chunks = await chunker.splitText(sanitizeText(content)) + + // 2. 生成向量 + const vectors = await presenter.llmproviderPresenter.getEmbeddings( + this.config.embedding.providerId, + this.config.embedding.modelId, + chunks + ) + + // 3. 插入向量 + await this.vectorP.insertVectors( + vectors.map((vector, index) => ({ + vector, + metadata: { + from: fileMessage.name, + filePath: fileMessage.path, + content: chunks[index] + }, + fileId: fileMessage.id + })) + ) + + // 4. 更新状态为完成 + fileMessage.status = 'completed' + await this.vectorP.updateFile(fileMessage) + } catch (err) { + // 出错时更新状态并记录原因 + fileMessage.status = 'error' + fileMessage.metadata.reason = String(err) + await this.vectorP.updateFile(fileMessage) + console.error('addFile 后台处理失败:', err) + } + + return fileMessage + } + + async similarityQuery(key: string): Promise { + try { + const embedding = await presenter.llmproviderPresenter.getEmbeddings( + this.config.embedding.providerId, + this.config.embedding.modelId, + [sanitizeText(key)] + ) + + return await this.vectorP.similarityQuery(embedding[0], { + topK: this.config.fragmentsNumber, + metric: this.config.normalized ? 'cosine' : 'ip' + }) + } catch (err) { + console.error( + `[RAG] Similarity query failed in knowledge base ${this.config.id} for key "${key}":`, + err + ) + throw err + } + } + + async deleteFile(fileId: string): Promise { + try { + await this.vectorP.deleteVectorsByFile(fileId) + await this.vectorP.deleteFile(fileId) + } catch (err) { + console.error( + `[RAG] Failed to delete file ${fileId} in knowledge base ${this.config.id}:`, + err + ) + throw err + } + } + + async reAddFile( + fileId: string + ): Promise<{ data: KnowledgeFileMessage; task: Promise }> { + const file = await this.queryFile(fileId) + if (file == null) { + throw new Error('文件不存在,请重新打开知识库后再试') + } + await this.vectorP.deleteVectorsByFile(fileId) + return this.addFile(file.path, fileId) + } + + async queryFile(fileId: string): Promise { + try { + return await this.vectorP.queryFile(fileId) + } catch (err) { + console.error( + `[RAG] Failed to query file ${fileId} in knowledge base ${this.config.id}:`, + err + ) + throw err + } + } + async listFiles(): Promise { + try { + return await this.vectorP.listFiles() + } catch (err) { + console.error(`[RAG] Failed to list files in knowledge base ${this.config.id}:`, err) + throw err + } + } + + async recoverProcessingFiles(): Promise { + const processingFiles = await this.vectorP.queryFiles({ status: 'processing' }) + for (const file of processingFiles) { + try { + file.status = 'error' + file.metadata.reason = '用户取消任务' + await this.vectorP.updateFile(file) + } catch (err) { + console.error( + `[RAG] Failed to recover processing file ${file.id} in knowledge base ${this.config.id}:`, + err + ) + } + } + } + + async destroy(): Promise { + try { + this.vectorP.destroy() + } catch (err) { + console.error(`[RAG] Error destroying knowledge base ${this.config.id}:`, err) + } + } + + async close(): Promise { + try { + this.vectorP.close() + } catch (err) { + console.error(`[RAG] Error closing knowledge base ${this.config.id}:`, err) + } + } +} diff --git a/src/main/presenter/llmProviderPresenter/baseProvider.ts b/src/main/presenter/llmProviderPresenter/baseProvider.ts index ed4741ac9..9678c8fcd 100644 --- a/src/main/presenter/llmProviderPresenter/baseProvider.ts +++ b/src/main/presenter/llmProviderPresenter/baseProvider.ts @@ -6,7 +6,8 @@ import { LLMCoreStreamEvent, ModelConfig, ChatMessage, - KeyStatus + KeyStatus, + LLM_EMBEDDING_ATTRS } from '@shared/presenter' import { ConfigPresenter } from '../configPresenter' import { DevicePresenter } from '../devicePresenter' @@ -605,13 +606,22 @@ ${this.convertToolsToXml(tools)} /** * 获取文本的 embedding 表示 - * @param texts 待编码的文本数组 - * @param modelId 使用的模型ID + * @param _texts 待编码的文本数组 + * @param _modelId 使用的模型ID * @returns embedding 数组,每个元素为 number[] */ // eslint-disable-next-line @typescript-eslint/no-unused-vars public async getEmbeddings(_texts: string[], _modelId: string): Promise { - throw new Error('getEmbeddings is not supported by this provider') + throw new Error('embedding is not supported by this provider') + } + + /** + * 获取嵌入向量的维度 + * @param _modelId 模型ID + * @returns 嵌入向量的维度 + */ + public async getDimensions(_modelId: string): Promise { + throw new Error('embedding is not supported by this provider') } /** diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index a427a0e27..efa5794fc 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -7,7 +7,8 @@ import { OllamaModel, ChatMessage, LLMAgentEvent, - KeyStatus + KeyStatus, + LLM_EMBEDDING_ATTRS } from '@shared/presenter' import { BaseLLMProvider } from './baseProvider' import { OpenAIProvider } from './providers/openAIProvider' @@ -1205,15 +1206,42 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { /** * 获取文本的 embedding 表示 * @param providerId 提供商ID - * @param texts 文本数组 * @param modelId 模型ID + * @param texts 文本数组 * @returns embedding 数组 */ - async getEmbeddings(providerId: string, texts: string[], modelId: string): Promise { - const provider = this.getProviderInstance(providerId) - if (!provider.getEmbeddings) { + async getEmbeddings(providerId: string, modelId: string, texts: string[]): Promise { + try { + const provider = this.getProviderInstance(providerId) + return await provider.getEmbeddings(texts, modelId) + } catch (error) { + console.error(`${modelId} embedding 失败:`, error) throw new Error('当前 LLM 提供商未实现 embedding 能力') } - return provider.getEmbeddings(texts, modelId) + } + + /** + * 获取指定模型的 embedding 维度 + * @param providerId 提供商ID + * @param modelId 模型ID + * @returns 模型的 embedding 维度 + */ + async getDimensions( + providerId: string, + modelId: string + ): Promise<{ data: LLM_EMBEDDING_ATTRS; errorMsg?: string }> { + try { + const provider = this.getProviderInstance(providerId) + return { data: await provider.getDimensions(modelId) } + } catch (error) { + console.error(`获取模型 ${modelId} 的 embedding 维度失败:`, error) + return { + data: { + dimensions: 0, + normalized: false + }, + errorMsg: error instanceof Error ? error.message : String(error) + } + } } } diff --git a/src/main/presenter/llmProviderPresenter/providers/ollamaProvider.ts b/src/main/presenter/llmProviderPresenter/providers/ollamaProvider.ts index 080f92245..d0933d003 100644 --- a/src/main/presenter/llmProviderPresenter/providers/ollamaProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/ollamaProvider.ts @@ -7,12 +7,14 @@ import { MCPToolDefinition, ModelConfig, LLMCoreStreamEvent, - ChatMessage + ChatMessage, + LLM_EMBEDDING_ATTRS } from '@shared/presenter' import { BaseLLMProvider, SUMMARY_TITLES_PROMPT } from '../baseProvider' import { ConfigPresenter } from '../../configPresenter' import { Ollama, Message, ShowResponse } from 'ollama' import { presenter } from '@/presenter' +import { EMBEDDING_TEST_KEY, isNormalized } from '@/utils/vector' // 定义 Ollama 工具类型 interface OllamaTool { @@ -287,12 +289,32 @@ export class OllamaProvider extends BaseLLMProvider { } } + private async attachModelInfo(model: OllamaModel): Promise { + const showResponse = await this.showModelInfo(model.name) + const info = showResponse.model_info + const family = model.details.family + const context_length = info[family + '.context_length'] + const embedding_length = info[family + '.embedding_length'] + const capabilities = showResponse.capabilities + + // 合并customConfig的属性到model + return { + ...model, + model_info: { + context_length, + embedding_length + }, + capabilities + } + } + // Ollama 特有的模型管理功能 public async listModels(): Promise { try { const response = await this.ollama.list() - // 返回类型转换,适应我们的 OllamaModel 接口 - return response.models as unknown as OllamaModel[] + const models = response.models as unknown as OllamaModel[] + // FIXME 合并模型属性,ollama list接口完善后优化 + return await Promise.all(models.map(async (model) => this.attachModelInfo(model))) } catch (error) { console.error('Failed to list Ollama models:', (error as Error).message) return [] @@ -302,7 +324,9 @@ export class OllamaProvider extends BaseLLMProvider { public async listRunningModels(): Promise { try { const response = await this.ollama.ps() - return response.models as unknown as OllamaModel[] + const runningModels = response.models as unknown as OllamaModel[] + // FIXME 合并模型属性,ollama list接口完善后优化 + return await Promise.all(runningModels.map(async (model) => this.attachModelInfo(model))) } catch (error) { console.error('Failed to list running Ollama models:', (error as Error).message) return [] @@ -1142,4 +1166,12 @@ export class OllamaProvider extends BaseLLMProvider { } return results } + + async getDimensions(modelId: string): Promise { + const res = await this.getEmbeddings([EMBEDDING_TEST_KEY], modelId) + return { + dimensions: res[0].length, + normalized: isNormalized(res[0]) + } + } } diff --git a/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts b/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts index 2319c793d..e42c42220 100644 --- a/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts @@ -1,3 +1,4 @@ +import { EMBEDDING_TEST_KEY, isNormalized } from '@/utils/vector' import { LLM_PROVIDER, LLMResponse, @@ -5,7 +6,8 @@ import { MCPToolDefinition, LLMCoreStreamEvent, ModelConfig, - ChatMessage + ChatMessage, + LLM_EMBEDDING_ATTRS } from '@shared/presenter' import { BaseLLMProvider, SUMMARY_TITLES_PROMPT } from '../baseProvider' import OpenAI, { AzureOpenAI } from 'openai' @@ -1279,21 +1281,38 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { } } - /** - * 获取文本的 embedding 表示 - * @param texts 待编码的文本数组 - * @param modelId 使用的模型ID - * @returns embedding 数组 - */ async getEmbeddings(texts: string[], modelId: string): Promise { if (!this.isInitialized) throw new Error('Provider not initialized') if (!modelId) throw new Error('Model ID is required') // OpenAI embeddings API const response = await this.openai.embeddings.create({ model: modelId, - input: texts + input: texts, + encoding_format: 'float' }) // 兼容 OpenAI 返回格式 return response.data.map((item) => item.embedding) } + + async getDimensions(modelId: string): Promise { + switch (modelId) { + case 'text-embedding-3-small': + case 'text-embedding-ada-002': + return { + dimensions: 1536, + normalized: true + } + case 'text-embedding-3-large': + return { + dimensions: 3072, + normalized: true + } + default: + const embeddings = await this.getEmbeddings([EMBEDDING_TEST_KEY], modelId) + return { + dimensions: embeddings[0].length, + normalized: isNormalized(embeddings[0]) + } + } + } } diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts b/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts index b44841297..4bc15262d 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts @@ -12,6 +12,8 @@ import { DeepResearchServer } from './deepResearchServer' import { AutoPromptingServer } from './autoPromptingServer' import { ConversationSearchServer } from './conversationSearchServer' import { MeetingServer } from './meetingServer' +import { BuiltinKnowledgeServer } from './builtinKnowledgeServer' +import { BuiltinKnowledgeConfig } from '@shared/presenter' import { AppleServer } from './appleServer' export function getInMemoryServer( @@ -70,6 +72,12 @@ export function getInMemoryServer( }[] } ) + case 'builtinKnowledge': + return new BuiltinKnowledgeServer( + env as { + configs: BuiltinKnowledgeConfig[] + } + ) case 'deepchat-inmemory/custom-prompts-server': return new CustomPromptsServer() case 'deepchat-inmemory/deep-research-server': diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/builtinKnowledgeServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/builtinKnowledgeServer.ts new file mode 100644 index 000000000..8c3a4570e --- /dev/null +++ b/src/main/presenter/mcpPresenter/inMemoryServers/builtinKnowledgeServer.ts @@ -0,0 +1,150 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { zodToJsonSchema } from 'zod-to-json-schema' +import { Transport } from '@modelcontextprotocol/sdk/shared/transport' +import { BuiltinKnowledgeConfig, MCPTextContent, QueryResult } from '@shared/presenter' +import { presenter } from '@/presenter' + +// Schema definitions +const BuiltinKnowledgeSearchArgsSchema = z.object({ + query: z.string().describe('搜索查询内容 (必填)'), + topK: z.number().optional().default(5).describe('返回结果数量 (默认5条)') +}) + +export class BuiltinKnowledgeServer { + private server: Server + private configs: Array = [] + + constructor(env?: { + configs: BuiltinKnowledgeConfig[] + }) { + if (!env) { + throw new Error('需要提供Builtin知识库配置') + } + const configs = env.configs + if (!Array.isArray(configs) || configs.length === 0) { + throw new Error('需要提供至少一个Builtin知识库配置') + } + for (const config of configs) { + if (!config.description) { + throw new Error('需要提供对这个知识库的描述,以方便ai决定是否检索此知识库') + } + this.configs.push(config) + } + this.server = new Server( + { + name: 'deepchat-inmemory/builtin-knowledge-server', + version: '0.1.0' + }, + { + capabilities: { + tools: {} + } + } + ) + this.setupRequestHandlers() + } + + public startServer(transport: Transport): void { + this.server.connect(transport) + } + + private setupRequestHandlers(): void { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + const tools = this.configs + .filter((conf) => conf.enabled) + .map((config, index) => { + const suffix = this.configs.length > 1 ? `_${index + 1}` : '' + return { + name: `builtin_knowledge_search${suffix}`, + description: config.description, + inputSchema: zodToJsonSchema(BuiltinKnowledgeSearchArgsSchema) + } + }) + return { tools } + }) + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: parameters } = request.params + if (name.startsWith('builtin_knowledge_search')) { + try { + const enabledConfigs = this.configs.filter((config) => config.enabled) + let configIndex = 0 + const match = name.match(/_([0-9]+)$/) + if (match) { + configIndex = parseInt(match[1], 10) - 1 + } + if (configIndex < 0 || configIndex >= enabledConfigs.length) { + throw new Error(`无效的知识库索引: ${configIndex}`) + } + return await this.performBuiltinKnowledgeSearch(parameters, configIndex) + } catch (error) { + return { + content: [ + { + type: 'text', + text: `搜索失败: ${error instanceof Error ? error.message : String(error)}` + } + ] + } + } + } + return { + content: [ + { + type: 'text', + text: `未知工具: ${name}` + } + ] + } + }) + } + + private async performBuiltinKnowledgeSearch( + parameters: Record | undefined, + configIndex: number = 0 + ): Promise<{ content: MCPTextContent[] }> { + const { query } = parameters as { query: string; topK?: number } + if (!query) { + throw new Error('查询内容不能为空') + } + try { + // 获取知识库 id(用 index 作为 id) + const config = this.configs[configIndex] + const id = config.id + // similarityQuery(id, key) + const results = await presenter.knowledgePresenter.similarityQuery(id, query) + let resultText = `### 查询: ${query}\n\n` + if (!results || results.length === 0) { + resultText += '未找到相关结果。' + } else { + resultText += `找到 ${results.length} 条相关结果:\n\n` + results.forEach((result: QueryResult, index: number) => { + resultText += `#### ${index + 1}. (ID: ${result.id})\n` + resultText += `${result.metadata.content || ''}\n\n` + if (result.metadata.filePath) { + resultText += `文件: ${result.metadata.filePath}\n` + } + resultText += `相似度: ${(1 - result.distance)}\n\n` + }) + } + return { + content: [ + { + type: 'text', + text: resultText + } + ] + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `搜索失败: ${error instanceof Error ? error.message : String(error)}` + } + ] + } + } + } +} diff --git a/src/main/utils/strings.ts b/src/main/utils/strings.ts new file mode 100644 index 000000000..015d2e9c5 --- /dev/null +++ b/src/main/utils/strings.ts @@ -0,0 +1,9 @@ +export function sanitizeText(text: string) { + text = text.replace(/\\/g, '') + text = text.replace(/#/g, ' ') + text = text.replace(/\. \./g, '.') + text = text.replace(/\s\s+/g, ' ') + text = text.replace(/(\r\n|\n|\r)/gm, ' ') + + return text.trim() +} diff --git a/src/main/utils/vector.ts b/src/main/utils/vector.ts new file mode 100644 index 000000000..ce5b4b5ae --- /dev/null +++ b/src/main/utils/vector.ts @@ -0,0 +1,55 @@ +export const EMBEDDING_TEST_KEY = 'sample' + +function calcNorm(vector: number[]): number { + return Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0)) +} + +/** + * 判断一个向量是否已 normalized(L2 范数 ≈ 1) + * @param vector 输入向量 + * @param tolerance 浮点误差容忍范围,默认 1e-3 + * @returns true 表示已 normalized + */ +export function isNormalized(vector: number[], tolerance = 1e-3): boolean { + if (!vector || !Array.isArray(vector) || vector.length === 0) return false + if (tolerance < 0) throw new Error('Tolerance must be non-negative') + if (vector.some((v) => typeof v !== 'number' || !isFinite(v))) return false + + const norm = calcNorm(vector) + return Math.abs(norm - 1) <= tolerance +} +/** + * 向量 normalized 处理 + * @param vector 输入向量 + * @returns normalized 向量 + */ +export function normalized(vector: number[]): number[] { + if (!vector || !Array.isArray(vector) || vector.length === 0) { + throw new Error('Vector cannot be empty') + } + const norm = calcNorm(vector) + if (norm === 0) { + throw new Error('Cannot normalize zero vector') + } + return vector.map((v) => v / norm) +} +/** + * 必定返回 normalized 向量 + * @param vector 输入向量 + * @param tolerance 浮点误差容忍范围,默认 1e-3 + * @returns normalized 向量 + */ +export function ensureNormalized(vector: number[], tolerance = 1e-3): number[] { + if (!vector || !Array.isArray(vector) || vector.length === 0) { + throw new Error('Vector cannot be empty') + } + if (tolerance < 0) throw new Error('Tolerance must be non-negative') + const norm = calcNorm(vector) + if (norm === 0) { + throw new Error('Cannot normalize zero vector') + } + if (Math.abs(norm - 1) <= tolerance) { + return vector + } + return vector.map((v) => v / norm) +} diff --git a/src/renderer/src/components/FileItem.vue b/src/renderer/src/components/FileItem.vue index d67a983ee..398fe1536 100644 --- a/src/renderer/src/components/FileItem.vue +++ b/src/renderer/src/components/FileItem.vue @@ -44,6 +44,7 @@ diff --git a/src/renderer/src/components/ModelSelect.vue b/src/renderer/src/components/ModelSelect.vue index d31f5ae29..2ee2de6a0 100644 --- a/src/renderer/src/components/ModelSelect.vue +++ b/src/renderer/src/components/ModelSelect.vue @@ -68,7 +68,7 @@ const props = defineProps({ }) const filteredProviders = computed(() => { - if (!keyword.value) return providers.value + if (!keyword.value) return providers.value.filter((provider) => provider.models.length > 0) return providers.value .map((provider) => ({ ...provider, diff --git a/src/renderer/src/components/mcp-config/components/McpServers.vue b/src/renderer/src/components/mcp-config/components/McpServers.vue index f8ef5b9a5..0ac1851d7 100644 --- a/src/renderer/src/components/mcp-config/components/McpServers.vue +++ b/src/renderer/src/components/mcp-config/components/McpServers.vue @@ -181,7 +181,8 @@ const openEditServerDialog = (serverName: string) => { const specialServers = { difyKnowledge: 'dify', ragflowKnowledge: 'ragflow', - fastGptKnowledge: 'fastgpt' + fastGptKnowledge: 'fastgpt', + builtinKnowledge: 'builtinKnowledge' } if (specialServers[serverName]) { diff --git a/src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue b/src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue new file mode 100644 index 000000000..9be487cdd --- /dev/null +++ b/src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue @@ -0,0 +1,884 @@ + + + diff --git a/src/renderer/src/components/settings/DifyKnowledgeSettings.vue b/src/renderer/src/components/settings/DifyKnowledgeSettings.vue index 9eb765e52..852608fb6 100644 --- a/src/renderer/src/components/settings/DifyKnowledgeSettings.vue +++ b/src/renderer/src/components/settings/DifyKnowledgeSettings.vue @@ -122,11 +122,14 @@ ? t('settings.knowledgeBase.editDifyConfig') : t('settings.knowledgeBase.addDifyConfig') }} + + {{ t('settings.knowledgeBase.difyDescription') }} +
+ + {{ t('settings.knowledgeBase.fastgptDescription') }} +
-
+
@@ -28,6 +28,8 @@ + +
-

Dify知识库

+

{{ t('settings.knowledgeBase.dify') }}

{{ t('settings.knowledgeBase.difyDescription') }}

@@ -64,7 +66,7 @@ >
-

RAGFlow

+

{{ t('settings.knowledgeBase.ragflowTitle') }}

{{ t('settings.knowledgeBase.ragflowDescription') }}

@@ -76,12 +78,26 @@ >
-

FastGPT

+

{{ t('settings.knowledgeBase.fastgptTitle') }}

{{ t('settings.knowledgeBase.fastgptDescription') }}

+
+ +
+

+ {{ t('settings.knowledgeBase.builtInKnowledgeTitle') }} +

+

+ {{ t('settings.knowledgeBase.builtInKnowledgeDescription') }} +

+
+
+
+ +
@@ -111,12 +134,23 @@ import { import RagflowKnowledgeSettings from './RagflowKnowledgeSettings.vue' import DifyKnowledgeSettings from './DifyKnowledgeSettings.vue' import FastGptKnowledgeSettings from './FastGptKnowledgeSettings.vue' +import BuiltinKnowledgeSettings from './BuiltinKnowledgeSettings.vue' +import KnowledgeFile from './KnowledgeFile.vue' +import { BuiltinKnowledgeConfig } from '@shared/presenter' const difySettingsRef = ref | null>(null) const ragflowSettingsRef = ref | null>(null) const fastGptSettingsRef = ref | null>(null) +const builtinSettingsRef = ref | null>(null) const { t } = useI18n() +// 是否展示内置知识库文件详情 +const showBuiltinKnowledgeDetail = ref(false) +const builtinKnowledgeDetail = ref(null) +const showDetail = (detail: BuiltinKnowledgeConfig) => { + showBuiltinKnowledgeDetail.value = true + builtinKnowledgeDetail.value = detail +} // 对话框状态 const isAddKnowledgeBaseDialogOpen = ref(false) @@ -134,6 +168,11 @@ const closeAddKnowledgeBaseDialog = () => { // 选择知识库类型 const selectKnowledgeBaseType = (type: string) => { closeAddKnowledgeBaseDialog() + if (type === 'builtinKnowledge') { + if (builtinSettingsRef.value) { + builtinSettingsRef.value.openAddConfig() + } + } if (type === 'dify') { if (difySettingsRef.value) { difySettingsRef.value.openAddConfig() diff --git a/src/renderer/src/components/settings/KnowledgeFile.vue b/src/renderer/src/components/settings/KnowledgeFile.vue new file mode 100644 index 000000000..68c6e2e98 --- /dev/null +++ b/src/renderer/src/components/settings/KnowledgeFile.vue @@ -0,0 +1,340 @@ + + + diff --git a/src/renderer/src/components/settings/KnowledgeFileItem.vue b/src/renderer/src/components/settings/KnowledgeFileItem.vue new file mode 100644 index 000000000..eddaae01d --- /dev/null +++ b/src/renderer/src/components/settings/KnowledgeFileItem.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/renderer/src/components/settings/ModelConfigItem.vue b/src/renderer/src/components/settings/ModelConfigItem.vue index ccbd5fa42..31cce48ec 100644 --- a/src/renderer/src/components/settings/ModelConfigItem.vue +++ b/src/renderer/src/components/settings/ModelConfigItem.vue @@ -40,6 +40,7 @@