refactor: 更新依赖并迁移至新的LangChain模块结构

- 将langchain依赖替换为@langchain/core和@langchain/textsplitters
- 更新FileLoader.ts以使用新的模块导入路径
- 调整eslint规则配置
- 在项目规则文档中添加类型检查规范
This commit is contained in:
hy committed 2025-12-23 16:11:10 +08:00
1 parent 08e1b84361
commit dc4ecfb0fa
6 files changed
+71 -169

No files matched your search

+1
View File
@@ -62,6 +62,7 @@ NCurator-Desktop 是一个基于 Electron-Vite 构建的本地知识库搜索应
- **IPC**: 使用 `ipcMain` and `ipcRenderer` (通过 `preload`) 在 UI 和主进程中的繁重处理逻辑之间进行通信。
- **模块化**: 将解析、索引和搜索逻辑与 UI 组件分离。
- **注释**: 注释复杂的逻辑,特别是 RRF 算法和模型加载步骤。
- **类型**: 禁止使用 `any` 类型,必须使用明确的类型注解。
## 6. AI 交互规则
- 生成代码时,优先考虑技术栈中列出的库。
+5 -1
View File
@@ -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
+2 -1
View File
@@ -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"
},
+8 -114
View File
@@ -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
+25 -25
View File
@@ -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<Document[]> {
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<Document[]> {
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
}
}
+30 -28
View File
@@ -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<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState<any[]>([])
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 (
<div className="p-8 max-w-4xl mx-auto h-screen flex flex-col">
@@ -71,7 +71,9 @@ function App(): React.JSX.Element {
renderItem={(item) => (
<List.Item>
<Card
title={<span className="text-sm text-gray-500">Score: {item.score?.toFixed(4)}</span>}
title={
<span className="text-sm text-gray-500">Score: {item.score?.toFixed(4)}</span>
}
className="w-full shadow-sm hover:shadow-md transition-shadow"
>
<Paragraph ellipsis={{ rows: 3, expandable: true, symbol: 'more' }}>
@@ -88,7 +90,7 @@ function App(): React.JSX.Element {
/>
</div>
</div>
);
)
}
export default App;
export default App