diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 2309f4d3..fd549ad1 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -62,6 +62,7 @@ NCurator-Desktop 是一个基于 Electron-Vite 构建的本地知识库搜索应 - **IPC**: 使用 `ipcMain` and `ipcRenderer` (通过 `preload`) 在 UI 和主进程中的繁重处理逻辑之间进行通信。 - **模块化**: 将解析、索引和搜索逻辑与 UI 组件分离。 - **注释**: 注释复杂的逻辑,特别是 RRF 算法和模型加载步骤。 +- **类型**: 禁止使用 `any` 类型,必须使用明确的类型注解。 ## 6. AI 交互规则 - 生成代码时,优先考虑技术栈中列出的库。 diff --git a/eslint.config.mjs b/eslint.config.mjs index aff5d3f9..e947ca97 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,11 @@ export default defineConfig( }, rules: { ...eslintPluginReactHooks.configs.recommended.rules, - ...eslintPluginReactRefresh.configs.vite.rules + ...eslintPluginReactRefresh.configs.vite.rules, + '@typescript-eslint/explicit-function-return-type': 0, + 'prettier/prettier': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-empty-function': 0 } }, eslintConfigPrettier diff --git a/package.json b/package.json index 12912560..9f87ccfb 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,11 @@ "@huggingface/transformers": "^3.8.1", "@lancedb/lancedb": "^0.23.0", "@langchain/community": "^1.1.1", + "@langchain/core": "^1.1.8", + "@langchain/textsplitters": "^1.0.1", "antd": "5.22.1", "electron-updater": "^6.3.9", "flexsearch": "^0.8.212", - "langchain": "^1.2.3", "mammoth": "^1.11.0", "pdf-parse": "^2.4.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdeecaa8..36f30505 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: '@langchain/community': specifier: ^1.1.1 version: 1.1.1(@browserbasehq/sdk@2.6.0(encoding@0.1.13))(@browserbasehq/stagehand@1.14.0(@playwright/test@1.57.0)(deepmerge@4.3.1)(dotenv@16.6.1)(encoding@0.1.13)(openai@6.15.0(ws@8.18.3)(zod@4.2.1))(zod@4.2.1))(@huggingface/transformers@3.8.1)(@ibm-cloud/watsonx-ai@1.7.5)(@lancedb/lancedb@0.23.0(apache-arrow@18.1.0))(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(ibm-cloud-sdk-core@5.4.5)(ignore@5.3.2)(jsonwebtoken@9.0.3)(lodash@4.17.21)(mammoth@1.11.0)(openai@6.15.0(ws@8.18.3)(zod@4.2.1))(pdf-parse@2.4.5)(playwright@1.57.0)(ws@8.18.3) + '@langchain/core': + specifier: ^1.1.8 + version: 1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) + '@langchain/textsplitters': + specifier: ^1.0.1 + version: 1.0.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1))) antd: specifier: 5.22.1 version: 5.22.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -32,9 +38,6 @@ importers: flexsearch: specifier: ^0.8.212 version: 0.8.212 - langchain: - specifier: ^1.2.3 - version: 1.2.3(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(openai@6.15.0(ws@8.18.3)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.0(zod@4.2.1)) mammoth: specifier: ^1.11.0 version: 1.11.0 @@ -921,7 +924,6 @@ packages: '@lancedb/lancedb@0.23.0': resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==} engines: {node: '>= 18'} - cpu: [x64, arm64] os: [darwin, linux, win32] peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' @@ -1308,37 +1310,6 @@ packages: resolution: {integrity: sha512-kIUidOgc0ZdyXo4Ahn9Zas+OayqOfk4ZoKPi7XaDipNSWSApc2+QK5BVcjvwtzxstsNOrmXJiJWEN6WPF/MvAw==} engines: {node: '>=20'} - '@langchain/langgraph-checkpoint@1.0.0': - resolution: {integrity: sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.0.1 - - '@langchain/langgraph-sdk@1.3.1': - resolution: {integrity: sha512-zTi7DZHwqtMEzapvm3I1FL4Q7OZsxtq9tTXy6s2gcCxyIU3sphqRboqytqVN7dNHLdTCLb8nXy49QKurs2MIBg==} - peerDependencies: - '@langchain/core': ^1.0.1 - react: ^18 || ^19 - react-dom: ^18 || ^19 - peerDependenciesMeta: - '@langchain/core': - optional: true - react: - optional: true - react-dom: - optional: true - - '@langchain/langgraph@1.0.7': - resolution: {integrity: sha512-EBGqNOWoRiEoLUaeuiXRpUM8/DE6QcwiirNyd97XhezStebBoTTilWH8CUt6S94JRGl5zwfBBRHfzotDnZS/eA==} - engines: {node: '>=18'} - peerDependencies: - '@langchain/core': ^1.0.1 - zod: ^3.25.32 || ^4.1.0 - zod-to-json-schema: ^3.x - peerDependenciesMeta: - zod-to-json-schema: - optional: true - '@langchain/openai@1.2.0': resolution: {integrity: sha512-r2g5Be3Sygw7VTJ89WVM/M94RzYToNTwXf8me1v+kgKxzdHbd/8XPYDFxpXEp3REyPgUrtJs+Oplba9pkTH5ug==} engines: {node: '>=20'} @@ -1839,9 +1810,6 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -3194,12 +3162,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - langchain@1.2.3: - resolution: {integrity: sha512-3k986xJuqg4az53JxV5LnGlOzIXF1d9Kq6Y9s7XjitvzhpsbFuTDV5/kiF4cx3pkNGyw0mUXC4tLz9RxucO0hw==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': 1.1.8 - langsmith@0.4.2: resolution: {integrity: sha512-BvBeFgSmR9esl8x5wsiDlALiHKKPybw2wE2Hh6x1tgSZki46H9c9KI9/06LARbPhyyDu/TZU7exfg6fnhdj1Qg==} peerDependencies: @@ -3668,10 +3630,6 @@ packages: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} @@ -4159,10 +4117,6 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -4585,10 +4539,6 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - verror@1.10.1: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} @@ -5572,34 +5522,6 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))': - dependencies: - '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) - uuid: 10.0.0 - - '@langchain/langgraph-sdk@1.3.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 9.0.1 - optionalDependencies: - '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - '@langchain/langgraph@1.0.7(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.0(zod@4.2.1))(zod@4.2.1)': - dependencies: - '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1))) - '@langchain/langgraph-sdk': 1.3.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - uuid: 10.0.0 - zod: 4.2.1 - optionalDependencies: - zod-to-json-schema: 3.25.0(zod@4.2.1) - transitivePeerDependencies: - - react - - react-dom - '@langchain/openai@1.2.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(ws@8.18.3)': dependencies: '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) @@ -6017,8 +5939,6 @@ snapshots: dependencies: '@types/node': 22.19.3 - '@types/retry@0.12.0': {} - '@types/tough-cookie@4.0.5': {} '@types/uuid@10.0.0': {} @@ -7531,7 +7451,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.3 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.13.2) + retry-axios: 2.6.0(axios@1.13.2(debug@4.4.3)) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -7820,23 +7740,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - langchain@1.2.3(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(openai@6.15.0(ws@8.18.3)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.0(zod@4.2.1)): - dependencies: - '@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) - '@langchain/langgraph': 1.0.7(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.0(zod@4.2.1))(zod@4.2.1) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.3)(zod@4.2.1))) - langsmith: 0.4.2(openai@6.15.0(ws@8.18.3)(zod@4.2.1)) - uuid: 10.0.0 - zod: 4.2.1 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - react - - react-dom - - zod-to-json-schema - langsmith@0.4.2(openai@6.15.0(ws@8.18.3)(zod@4.2.1)): dependencies: '@types/uuid': 10.0.0 @@ -8281,11 +8184,6 @@ snapshots: eventemitter3: 4.0.7 p-timeout: 3.2.0 - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - p-timeout@3.2.0: dependencies: p-finally: 1.0.0 @@ -8844,14 +8742,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.13.2): + retry-axios@2.6.0(axios@1.13.2(debug@4.4.3)): dependencies: axios: 1.13.2(debug@4.4.3) retry@0.12.0: {} - retry@0.13.1: {} - rimraf@2.6.3: dependencies: glob: 7.2.3 @@ -9378,8 +9274,6 @@ snapshots: uuid@10.0.0: {} - uuid@9.0.1: {} - verror@1.10.1: dependencies: assert-plus: 1.0.0 diff --git a/src/main/services/ingestion/FileLoader.ts b/src/main/services/ingestion/FileLoader.ts index 95da97b1..df7346db 100644 --- a/src/main/services/ingestion/FileLoader.ts +++ b/src/main/services/ingestion/FileLoader.ts @@ -1,55 +1,55 @@ -import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'; -import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; -import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'; -import { Document } from 'langchain/document'; +import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' +import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' +import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters' +import { Document } from '@langchain/core/documents' export class IngestionService { - private static instance: IngestionService; + private static instance: IngestionService private constructor() {} public static getInstance(): IngestionService { if (!IngestionService.instance) { - IngestionService.instance = new IngestionService(); + IngestionService.instance = new IngestionService() } - return IngestionService.instance; + return IngestionService.instance } public async loadFile(filePath: string): Promise { - let loader; + let loader if (filePath.endsWith('.pdf')) { loader = new PDFLoader(filePath, { - splitPages: false, // Combine pages or keep separate? Usually separate is better for granular chunks, but let's see. + splitPages: false // Combine pages or keep separate? Usually separate is better for granular chunks, but let's see. // Langchain default is one doc per page. - }); + }) } else if (filePath.endsWith('.docx')) { - loader = new DocxLoader(filePath); + loader = new DocxLoader(filePath) } else { - throw new Error('Unsupported file type'); + throw new Error('Unsupported file type') } - const docs = await loader.load(); - return docs; + const docs = await loader.load() + return docs } public async splitDocuments(docs: Document[]): Promise { const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, - chunkOverlap: 50, - }); + chunkOverlap: 50 + }) - const splitDocs = await splitter.splitDocuments(docs); - return splitDocs; + const splitDocs = await splitter.splitDocuments(docs) + return splitDocs } public async processFile(filePath: string) { - const docs = await this.loadFile(filePath); - const chunks = await this.splitDocuments(docs); + const docs = await this.loadFile(filePath) + const chunks = await this.splitDocuments(docs) // Add metadata - chunks.forEach(chunk => { - chunk.metadata.source = filePath; - chunk.metadata.filename = filePath.split('/').pop(); - }); - return chunks; + chunks.forEach((chunk) => { + chunk.metadata.source = filePath + chunk.metadata.filename = filePath.split('/').pop() + }) + return chunks } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 35d155a4..d14c2b8a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,50 +1,50 @@ -import React, { useState } from 'react'; -import { Input, Button, Upload, message, List, Card, Typography } from 'antd'; -import { UploadOutlined } from '@ant-design/icons'; +import React, { useState } from 'react' +import { Input, Button, Upload, message, List, Card, Typography } from 'antd' +import { UploadOutlined } from '@ant-design/icons' -const { Paragraph } = Typography; +const { Paragraph } = Typography function App(): React.JSX.Element { - const [searchResults, setSearchResults] = useState([]); - const [loading, setLoading] = useState(false); + const [searchResults, setSearchResults] = useState([]) + const [loading, setLoading] = useState(false) const handleSearch = async (value: string) => { - if (!value.trim()) return; - setLoading(true); + if (!value.trim()) return + setLoading(true) try { - const results = await window.api.search(value); - setSearchResults(results); + const results = await window.api.search(value) + setSearchResults(results) } catch (error) { - console.error(error); - message.error('Search failed'); + console.error(error) + message.error('Search failed') } finally { - setLoading(false); + setLoading(false) } - }; + } const handleUpload = async (file: any) => { // In Electron, File object has a 'path' property - const filePath = file.path; + const filePath = file.path if (!filePath) { - message.error('Could not get file path'); - return Upload.LIST_IGNORE; + message.error('Could not get file path') + return Upload.LIST_IGNORE } try { - message.loading({ content: 'Ingesting file...', key: 'ingest', duration: 0 }); - const result = await window.api.ingestFile(filePath); + message.loading({ content: 'Ingesting file...', key: 'ingest', duration: 0 }) + const result = await window.api.ingestFile(filePath) if (result.success) { - message.success({ content: `Ingested ${result.count} chunks`, key: 'ingest' }); + message.success({ content: `Ingested ${result.count} chunks`, key: 'ingest' }) } else { - message.error({ content: `Ingestion failed: ${result.error}`, key: 'ingest' }); + message.error({ content: `Ingestion failed: ${result.error}`, key: 'ingest' }) } } catch (error) { - console.error(error); - message.error({ content: 'Ingestion error', key: 'ingest' }); + console.error(error) + message.error({ content: 'Ingestion error', key: 'ingest' }) } - return Upload.LIST_IGNORE; // Prevent upload - }; + return Upload.LIST_IGNORE // Prevent upload + } return (
@@ -71,7 +71,9 @@ function App(): React.JSX.Element { renderItem={(item) => ( Score: {item.score?.toFixed(4)}} + title={ + Score: {item.score?.toFixed(4)} + } className="w-full shadow-sm hover:shadow-md transition-shadow" > @@ -88,7 +90,7 @@ function App(): React.JSX.Element { />
- ); + ) } -export default App; +export default App