@huyooo/ai-search 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +431 -0
- package/dist/bridge/electron.d.ts +51 -0
- package/dist/bridge/electron.js +10 -0
- package/dist/bridge/electron.js.map +1 -0
- package/dist/chunk-GAT4F5NK.js +176 -0
- package/dist/chunk-GAT4F5NK.js.map +1 -0
- package/dist/chunk-YJIIX54F.js +4239 -0
- package/dist/chunk-YJIIX54F.js.map +1 -0
- package/dist/index-B6UR8lRu.d.ts +576 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# @huyooo/ai-search
|
|
2
|
+
|
|
3
|
+
本地文档语义搜索引擎,支持 Word、PDF、Excel、TXT、MD 等文档格式。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🔍 **语义搜索**:基于向量的语义理解,不仅仅是关键词匹配
|
|
8
|
+
- 📄 **多格式支持**:Word (.docx/.doc)、PDF、Excel (.xlsx/.xls)、PPT (.pptx/.ppt)、TXT、MD
|
|
9
|
+
- 🏠 **本地优先**:所有数据存储在本地,保护隐私
|
|
10
|
+
- ⚡ **高性能**:LanceDB 向量搜索 + FlexSearch 全文检索
|
|
11
|
+
- 🤖 **AI 集成**:可作为 AI Agent 工具使用
|
|
12
|
+
- 🔗 **兼容 file-explorer**:搜索结果兼容 FileItem 数据结构
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @huyooo/ai-search
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 快速开始
|
|
21
|
+
|
|
22
|
+
### 环境变量
|
|
23
|
+
|
|
24
|
+
使用前需要设置豆包 API Key:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export ARK_API_KEY=your_api_key_here
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 基础使用
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { DocumentSearch } from '@huyooo/ai-search'
|
|
34
|
+
|
|
35
|
+
// 创建搜索引擎
|
|
36
|
+
const search = new DocumentSearch({
|
|
37
|
+
dataDir: './search-data', // 索引数据存储目录
|
|
38
|
+
// arkApiKey: 'your_key', // 也可以直接传入,默认从环境变量读取
|
|
39
|
+
// embeddingDimension: 1024, // 向量维度(可选,默认 1024)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 初始化
|
|
43
|
+
await search.init()
|
|
44
|
+
|
|
45
|
+
// 索引目录
|
|
46
|
+
await search.indexDirectory('~/Documents')
|
|
47
|
+
|
|
48
|
+
// 搜索
|
|
49
|
+
const results = await search.search('去年的采购合同')
|
|
50
|
+
|
|
51
|
+
console.log(results)
|
|
52
|
+
// [
|
|
53
|
+
// {
|
|
54
|
+
// id: '/Users/xxx/Documents/采购合同.docx',
|
|
55
|
+
// name: '采购合同.docx',
|
|
56
|
+
// type: 'document',
|
|
57
|
+
// size: '1.2 MB',
|
|
58
|
+
// dateModified: '2024-03-15',
|
|
59
|
+
// score: 0.92,
|
|
60
|
+
// snippet: '...甲方与乙方就采购事宜...',
|
|
61
|
+
// },
|
|
62
|
+
// ...
|
|
63
|
+
// ]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 作为 AI Agent 工具(推荐)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { searchPlugin } from '@huyooo/ai-search'
|
|
70
|
+
|
|
71
|
+
// Vite 插件风格 API
|
|
72
|
+
const plugin = await searchPlugin({
|
|
73
|
+
dataDir: './search-data',
|
|
74
|
+
workspace: '~/Documents', // 可选:初始工作空间
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// 注册工具到 Agent
|
|
78
|
+
agent.registerTools(plugin.tools)
|
|
79
|
+
|
|
80
|
+
// Agent 现在可以使用所有工具,包括:
|
|
81
|
+
// - search_local_documents: 搜索文档
|
|
82
|
+
// - index_document_file: 索引单个文件
|
|
83
|
+
// - index_document_directory: 索引目录
|
|
84
|
+
// - index_document_files: 批量索引文件
|
|
85
|
+
// - watch_document_directory: 监听目录变化
|
|
86
|
+
// - export_document_index: 导出索引
|
|
87
|
+
// - optimize_document_index: 优化索引
|
|
88
|
+
// ... 等 20+ 个工具
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 搜索选项
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const results = await search.search('合同', {
|
|
95
|
+
limit: 20, // 返回数量
|
|
96
|
+
mode: 'hybrid', // semantic | keyword | hybrid
|
|
97
|
+
fileTypes: ['document', 'pdf'], // 文件类型过滤
|
|
98
|
+
dateRange: { // 时间范围
|
|
99
|
+
start: new Date('2024-01-01'),
|
|
100
|
+
end: new Date('2024-12-31'),
|
|
101
|
+
},
|
|
102
|
+
directories: ['/path/to/docs'], // 目录过滤
|
|
103
|
+
sizeRange: { // 文件大小过滤
|
|
104
|
+
min: 1024, // 最小 1KB
|
|
105
|
+
max: 10 * 1024 * 1024, // 最大 10MB
|
|
106
|
+
},
|
|
107
|
+
fileNamePattern: '*.pdf', // 文件名模式(通配符)
|
|
108
|
+
titleContains: '合同', // 标题包含
|
|
109
|
+
combineMode: 'AND', // 组合条件模式
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 文件监听和自动更新
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// 监听目录变化,自动更新索引
|
|
117
|
+
search.watchDirectory('/path/to/documents', {
|
|
118
|
+
ignoreInitial: true, // 忽略初始扫描
|
|
119
|
+
debounce: 1000, // 防抖延迟(毫秒)
|
|
120
|
+
onEvent: (event) => {
|
|
121
|
+
console.log('文件变化:', event.type, event.path)
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 停止监听
|
|
126
|
+
search.unwatchDirectory('/path/to/documents')
|
|
127
|
+
|
|
128
|
+
// 获取正在监听的目录
|
|
129
|
+
const watched = search.getWatchedDirectories()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 批量操作
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// 批量索引文件
|
|
136
|
+
const result = await search.indexFiles([
|
|
137
|
+
'/path/to/file1.pdf',
|
|
138
|
+
'/path/to/file2.docx',
|
|
139
|
+
], (progress) => {
|
|
140
|
+
console.log(`进度: ${progress.indexed}/${progress.total}`)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// 批量删除索引
|
|
144
|
+
await search.removeFiles(['/path/to/file1.pdf', '/path/to/file2.docx'])
|
|
145
|
+
|
|
146
|
+
// 批量更新索引(重新索引)
|
|
147
|
+
await search.updateFiles(['/path/to/file1.pdf'])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 索引导出和导入
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// 导出索引(备份)
|
|
154
|
+
const exportInfo = await search.exportIndex('./backup/search-index')
|
|
155
|
+
|
|
156
|
+
// 导入索引(恢复)
|
|
157
|
+
await search.importIndex('./backup/search-index')
|
|
158
|
+
|
|
159
|
+
// 列出备份
|
|
160
|
+
const backups = await search.listBackups('./backup')
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 索引优化和维护
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// 清理无效索引(文件已删除但索引还在)
|
|
167
|
+
const result = await search.cleanup()
|
|
168
|
+
console.log(`清理了 ${result.removed} 个无效索引`)
|
|
169
|
+
|
|
170
|
+
// 优化索引(压缩、碎片整理)
|
|
171
|
+
await search.optimize()
|
|
172
|
+
|
|
173
|
+
// 健康检查
|
|
174
|
+
const health = await search.healthCheck()
|
|
175
|
+
if (!health.healthy) {
|
|
176
|
+
console.log('索引存在问题,建议执行 optimize')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 获取索引错误
|
|
180
|
+
const errors = search.getIndexErrors()
|
|
181
|
+
|
|
182
|
+
// 重试失败的索引
|
|
183
|
+
await search.retryFailedIndexes()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## API
|
|
187
|
+
|
|
188
|
+
### DocumentSearch
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
class DocumentSearch {
|
|
192
|
+
constructor(config: SearchConfig)
|
|
193
|
+
|
|
194
|
+
// ========== 初始化 ==========
|
|
195
|
+
init(): Promise<void>
|
|
196
|
+
|
|
197
|
+
// ========== 索引 ==========
|
|
198
|
+
// 单文件索引
|
|
199
|
+
indexFile(filePath: string, retryCount?: number): Promise<void>
|
|
200
|
+
|
|
201
|
+
// 目录索引
|
|
202
|
+
indexDirectory(
|
|
203
|
+
dir: string,
|
|
204
|
+
onProgress?: (p: IndexProgress) => void
|
|
205
|
+
): Promise<void>
|
|
206
|
+
|
|
207
|
+
// 索引默认目录
|
|
208
|
+
indexDefaultDirectories(
|
|
209
|
+
onProgress?: (p: IndexProgress) => void
|
|
210
|
+
): Promise<void>
|
|
211
|
+
|
|
212
|
+
// 批量索引
|
|
213
|
+
indexFiles(
|
|
214
|
+
filePaths: string[],
|
|
215
|
+
onProgress?: (p: IndexProgress) => void
|
|
216
|
+
): Promise<BatchOperationResult>
|
|
217
|
+
|
|
218
|
+
// ========== 搜索 ==========
|
|
219
|
+
search(query: string, options?: SearchOptions): Promise<SearchResult[]>
|
|
220
|
+
|
|
221
|
+
// ========== 文件监听 ==========
|
|
222
|
+
watchDirectory(directory: string, options?: WatchOptions): void
|
|
223
|
+
unwatchDirectory(directory: string): void
|
|
224
|
+
unwatchAll(): void
|
|
225
|
+
getWatchedDirectories(): string[]
|
|
226
|
+
|
|
227
|
+
// ========== 批量操作 ==========
|
|
228
|
+
removeFiles(filePaths: string[]): Promise<BatchOperationResult>
|
|
229
|
+
updateFiles(
|
|
230
|
+
filePaths: string[],
|
|
231
|
+
onProgress?: (p: IndexProgress) => void
|
|
232
|
+
): Promise<BatchOperationResult>
|
|
233
|
+
|
|
234
|
+
// ========== 索引管理 ==========
|
|
235
|
+
removeFile(filePath: string): Promise<void>
|
|
236
|
+
getStats(): IndexStats
|
|
237
|
+
clear(): Promise<void>
|
|
238
|
+
save(): Promise<void>
|
|
239
|
+
|
|
240
|
+
// ========== 索引优化 ==========
|
|
241
|
+
cleanup(): Promise<{ removed: number; updated: number }>
|
|
242
|
+
optimize(): Promise<void>
|
|
243
|
+
healthCheck(): Promise<HealthCheckResult>
|
|
244
|
+
|
|
245
|
+
// ========== 错误处理 ==========
|
|
246
|
+
getIndexErrors(): IndexError[]
|
|
247
|
+
retryFailedIndexes(): Promise<BatchOperationResult>
|
|
248
|
+
|
|
249
|
+
// ========== 导出/导入 ==========
|
|
250
|
+
exportIndex(outputPath: string): Promise<ExportInfo>
|
|
251
|
+
importIndex(inputPath: string): Promise<void>
|
|
252
|
+
listBackups(backupDir: string): Promise<BackupInfo[]>
|
|
253
|
+
|
|
254
|
+
// ========== 资源清理 ==========
|
|
255
|
+
destroy(): Promise<void>
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### SearchResult(兼容 FileItem)
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
interface SearchResult {
|
|
263
|
+
// 与 file-explorer FileItem 兼容
|
|
264
|
+
id: string // 文件路径
|
|
265
|
+
name: string // 文件名
|
|
266
|
+
type: FileType // 文件类型
|
|
267
|
+
size?: string // 格式化大小
|
|
268
|
+
dateModified?: string// 格式化日期
|
|
269
|
+
url?: string // file:// URL
|
|
270
|
+
|
|
271
|
+
// 搜索特有字段
|
|
272
|
+
score: number // 相关度分数 (0-1)
|
|
273
|
+
snippet?: string // 匹配摘要
|
|
274
|
+
matchType: 'semantic' | 'keyword' | 'hybrid'
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### SearchOptions
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
interface SearchOptions {
|
|
282
|
+
limit?: number // 返回数量限制
|
|
283
|
+
mode?: 'semantic' | 'keyword' | 'hybrid' // 搜索模式
|
|
284
|
+
fileTypes?: FileType[] // 文件类型过滤
|
|
285
|
+
dateRange?: { // 时间范围过滤
|
|
286
|
+
start?: Date
|
|
287
|
+
end?: Date
|
|
288
|
+
}
|
|
289
|
+
directories?: string[] // 目录过滤
|
|
290
|
+
sizeRange?: { // 文件大小过滤
|
|
291
|
+
min?: number // 字节
|
|
292
|
+
max?: number // 字节
|
|
293
|
+
}
|
|
294
|
+
fileNamePattern?: string // 文件名模式(通配符 * ?)
|
|
295
|
+
titleContains?: string // 文档标题包含
|
|
296
|
+
combineMode?: 'AND' | 'OR' // 组合条件模式
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### SearchConfig
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
interface SearchConfig {
|
|
304
|
+
dataDir: string // 索引数据存储目录
|
|
305
|
+
indexDirectories?: string[] // 索引目录列表
|
|
306
|
+
excludeDirs?: string[] // 排除目录
|
|
307
|
+
extensions?: string[] // 支持的扩展名
|
|
308
|
+
maxFileSize?: number // 最大文件大小(字节)
|
|
309
|
+
embeddingModel?: string // Embedding 模型(默认 doubao-embedding-vision-250615)
|
|
310
|
+
arkApiKey?: string // 豆包 API Key(也可通过环境变量 ARK_API_KEY 设置)
|
|
311
|
+
embeddingDimension?: number // 向量维度(默认 1024)
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### AI Agent 工具列表
|
|
316
|
+
|
|
317
|
+
使用 `searchPlugin` 后,Agent 可以使用以下工具:
|
|
318
|
+
|
|
319
|
+
#### 搜索相关
|
|
320
|
+
- `search_local_documents` - 搜索文档
|
|
321
|
+
|
|
322
|
+
#### 索引相关
|
|
323
|
+
- `index_document_file` - 索引单个文件
|
|
324
|
+
- `index_document_directory` - 索引目录
|
|
325
|
+
- `index_document_files` - 批量索引文件
|
|
326
|
+
- `update_document_files` - 批量更新索引
|
|
327
|
+
|
|
328
|
+
#### 工作空间管理
|
|
329
|
+
- `set_search_workspace` - 设置工作空间
|
|
330
|
+
- `get_search_workspace` - 获取工作空间状态
|
|
331
|
+
|
|
332
|
+
#### 文件监听
|
|
333
|
+
- `watch_document_directory` - 监听目录变化
|
|
334
|
+
- `unwatch_document_directory` - 停止监听
|
|
335
|
+
- `get_watched_directories` - 获取监听列表
|
|
336
|
+
|
|
337
|
+
#### 索引管理
|
|
338
|
+
- `get_document_index_stats` - 获取统计信息
|
|
339
|
+
- `clear_document_index` - 清空索引
|
|
340
|
+
- `remove_document_files` - 批量删除索引
|
|
341
|
+
|
|
342
|
+
#### 索引优化
|
|
343
|
+
- `cleanup_document_index` - 清理无效索引
|
|
344
|
+
- `optimize_document_index` - 优化索引
|
|
345
|
+
- `check_document_index_health` - 健康检查
|
|
346
|
+
|
|
347
|
+
#### 错误处理
|
|
348
|
+
- `get_document_index_errors` - 获取索引错误
|
|
349
|
+
- `retry_failed_document_indexes` - 重试失败索引
|
|
350
|
+
|
|
351
|
+
#### 导出/导入
|
|
352
|
+
- `export_document_index` - 导出索引
|
|
353
|
+
- `import_document_index` - 导入索引
|
|
354
|
+
- `list_document_index_backups` - 列出备份
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## 特性详解
|
|
358
|
+
|
|
359
|
+
### 🔄 增量索引
|
|
360
|
+
- 自动检测文件变化(路径 + 修改时间 + 文件大小)
|
|
361
|
+
- 文件未变化时跳过索引,提升性能
|
|
362
|
+
- 支持内容哈希去重,相同内容复用索引
|
|
363
|
+
|
|
364
|
+
### 📁 文件监听
|
|
365
|
+
- 自动监听目录变化
|
|
366
|
+
- 新增/修改文件自动索引
|
|
367
|
+
- 删除文件自动清理索引
|
|
368
|
+
- 支持防抖,避免频繁触发
|
|
369
|
+
|
|
370
|
+
### 🚀 批量操作
|
|
371
|
+
- 批量索引、删除、更新
|
|
372
|
+
- 支持进度回调
|
|
373
|
+
- 错误处理和重试机制
|
|
374
|
+
|
|
375
|
+
### 💾 索引管理
|
|
376
|
+
- 导出/导入索引(备份和恢复)
|
|
377
|
+
- 索引优化和维护
|
|
378
|
+
- 健康检查和错误恢复
|
|
379
|
+
|
|
380
|
+
## 技术栈
|
|
381
|
+
|
|
382
|
+
| 组件 | 技术 |
|
|
383
|
+
|-----|-----|
|
|
384
|
+
| 向量存储 | LanceDB |
|
|
385
|
+
| 全文搜索 | FlexSearch |
|
|
386
|
+
| 元数据存储 | SQLite (better-sqlite3) |
|
|
387
|
+
| 文本向量化 | 豆包 doubao-embedding-vision-250615(多模态) |
|
|
388
|
+
| 中文分词 | nodejieba |
|
|
389
|
+
| 文档解析 | mammoth, pdf-parse, xlsx |
|
|
390
|
+
| 文件扫描 | fdir |
|
|
391
|
+
| 文件监听 | chokidar |
|
|
392
|
+
|
|
393
|
+
## 向量化模型
|
|
394
|
+
|
|
395
|
+
使用字节跳动的 **doubao-embedding-vision-250615** 多模态向量化模型:
|
|
396
|
+
|
|
397
|
+
- 支持文本、图片、视频向量化
|
|
398
|
+
- 支持多模态混合输入
|
|
399
|
+
- 默认 1024 维向量(可配置)
|
|
400
|
+
- 需要豆包 API Key(ARK_API_KEY)
|
|
401
|
+
|
|
402
|
+
### 高级向量化 API
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import {
|
|
406
|
+
embed, // 文本向量化
|
|
407
|
+
embedImage, // 图片向量化
|
|
408
|
+
embedVideo, // 视频向量化
|
|
409
|
+
embedMultimodal, // 多模态混合
|
|
410
|
+
} from '@huyooo/ai-search'
|
|
411
|
+
|
|
412
|
+
// 文本向量化
|
|
413
|
+
const textVector = await embed('天很蓝,海很深')
|
|
414
|
+
|
|
415
|
+
// 图片向量化
|
|
416
|
+
const imageVector = await embedImage('https://example.com/image.jpg')
|
|
417
|
+
|
|
418
|
+
// 视频向量化
|
|
419
|
+
const videoVector = await embedVideo('https://example.com/video.mp4')
|
|
420
|
+
|
|
421
|
+
// 多模态混合
|
|
422
|
+
const mixedVector = await embedMultimodal([
|
|
423
|
+
{ type: 'text', text: '这是什么?' },
|
|
424
|
+
{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } },
|
|
425
|
+
])
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## 许可证
|
|
429
|
+
|
|
430
|
+
MIT
|
|
431
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IpcMain } from 'electron';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Electron 桥接模块
|
|
5
|
+
*
|
|
6
|
+
* 使用全局进度监听器自动广播索引进度
|
|
7
|
+
* 无需包装方法,无需轮询,解决时序问题
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Electron 桥接选项 */
|
|
11
|
+
interface SearchElectronBridgeOptions {
|
|
12
|
+
/** IPC channel 前缀 */
|
|
13
|
+
channelPrefix?: string;
|
|
14
|
+
/** IPC Main 实例 */
|
|
15
|
+
ipcMain: IpcMain;
|
|
16
|
+
}
|
|
17
|
+
/** 索引进度广播系统 */
|
|
18
|
+
declare class SearchElectronBridge {
|
|
19
|
+
private channelPrefix;
|
|
20
|
+
private ipcMain;
|
|
21
|
+
private indexingListeners;
|
|
22
|
+
private lastProgress;
|
|
23
|
+
private isIndexing;
|
|
24
|
+
constructor(options: SearchElectronBridgeOptions);
|
|
25
|
+
/** 初始化桥接(注册 IPC handlers 和全局进度监听器) */
|
|
26
|
+
init(): void;
|
|
27
|
+
/** 注册全局进度监听器 */
|
|
28
|
+
private registerGlobalProgressListener;
|
|
29
|
+
/** 注册 IPC handlers */
|
|
30
|
+
private registerIpcHandlers;
|
|
31
|
+
/** 广播索引进度到所有前端监听器 */
|
|
32
|
+
private broadcastIndexProgress;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 创建搜索 Electron 桥接
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { createSearchElectronBridge } from '@huyooo/ai-search/bridge/electron';
|
|
40
|
+
* import { ipcMain } from 'electron';
|
|
41
|
+
*
|
|
42
|
+
* const bridge = createSearchElectronBridge({
|
|
43
|
+
* ipcMain,
|
|
44
|
+
* channelPrefix: 'ai-chat',
|
|
45
|
+
* });
|
|
46
|
+
* bridge.init();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function createSearchElectronBridge(options: SearchElectronBridgeOptions): SearchElectronBridge;
|
|
50
|
+
|
|
51
|
+
export { SearchElectronBridge, type SearchElectronBridgeOptions, createSearchElectronBridge };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addGlobalProgressListener,
|
|
3
|
+
getSearchPlugin
|
|
4
|
+
} from "./chunk-YJIIX54F.js";
|
|
5
|
+
|
|
6
|
+
// src/bridge/electron.ts
|
|
7
|
+
var SearchElectronBridge = class {
|
|
8
|
+
channelPrefix;
|
|
9
|
+
ipcMain;
|
|
10
|
+
indexingListeners = /* @__PURE__ */ new Set();
|
|
11
|
+
lastProgress = null;
|
|
12
|
+
isIndexing = false;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.channelPrefix = options.channelPrefix || "ai-chat";
|
|
15
|
+
this.ipcMain = options.ipcMain;
|
|
16
|
+
}
|
|
17
|
+
/** 初始化桥接(注册 IPC handlers 和全局进度监听器) */
|
|
18
|
+
init() {
|
|
19
|
+
this.registerIpcHandlers();
|
|
20
|
+
this.registerGlobalProgressListener();
|
|
21
|
+
console.log("[AI-Search] Electron \u6865\u63A5\u5DF2\u521D\u59CB\u5316");
|
|
22
|
+
}
|
|
23
|
+
/** 注册全局进度监听器 */
|
|
24
|
+
registerGlobalProgressListener() {
|
|
25
|
+
addGlobalProgressListener((progress) => {
|
|
26
|
+
this.isIndexing = progress.stage !== "done";
|
|
27
|
+
this.lastProgress = progress;
|
|
28
|
+
this.broadcastIndexProgress(progress);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** 注册 IPC handlers */
|
|
32
|
+
registerIpcHandlers() {
|
|
33
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:registerListener`, (event) => {
|
|
34
|
+
const webContents = event.sender;
|
|
35
|
+
this.indexingListeners.add(webContents);
|
|
36
|
+
if (this.lastProgress && this.isIndexing) {
|
|
37
|
+
if (!webContents.isDestroyed()) {
|
|
38
|
+
webContents.send(`${this.channelPrefix}:index:progress`, {
|
|
39
|
+
indexed: this.lastProgress.indexed,
|
|
40
|
+
total: this.lastProgress.total,
|
|
41
|
+
currentFile: this.lastProgress.currentFile,
|
|
42
|
+
stage: this.lastProgress.stage
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { success: true };
|
|
47
|
+
});
|
|
48
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:unregisterListener`, (event) => {
|
|
49
|
+
this.indexingListeners.delete(event.sender);
|
|
50
|
+
return { success: true };
|
|
51
|
+
});
|
|
52
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:getStats`, async () => {
|
|
53
|
+
try {
|
|
54
|
+
const searchPlugin = getSearchPlugin();
|
|
55
|
+
if (!searchPlugin) {
|
|
56
|
+
return {
|
|
57
|
+
totalDocuments: 0,
|
|
58
|
+
indexSize: 0,
|
|
59
|
+
lastUpdated: null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const stats = searchPlugin.search.getStats();
|
|
63
|
+
return {
|
|
64
|
+
totalDocuments: stats.totalDocuments,
|
|
65
|
+
indexSize: stats.indexSize,
|
|
66
|
+
lastUpdated: stats.lastUpdated ? stats.lastUpdated.toISOString() : null
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("[AI-Search] \u83B7\u53D6\u7D22\u5F15\u7EDF\u8BA1\u5931\u8D25:", error);
|
|
70
|
+
return {
|
|
71
|
+
totalDocuments: 0,
|
|
72
|
+
indexSize: 0,
|
|
73
|
+
lastUpdated: null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:sync`, async () => {
|
|
78
|
+
try {
|
|
79
|
+
const searchPlugin = getSearchPlugin();
|
|
80
|
+
if (!searchPlugin) {
|
|
81
|
+
throw new Error("\u641C\u7D22\u63D2\u4EF6\u672A\u521D\u59CB\u5316");
|
|
82
|
+
}
|
|
83
|
+
const workspaceState = searchPlugin.getWorkspaceState();
|
|
84
|
+
if (!workspaceState.directory) {
|
|
85
|
+
throw new Error("\u5DE5\u4F5C\u7A7A\u95F4\u672A\u8BBE\u7F6E");
|
|
86
|
+
}
|
|
87
|
+
await searchPlugin.search.indexDirectory(workspaceState.directory);
|
|
88
|
+
return { success: true };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
91
|
+
console.error("[AI-Search] \u540C\u6B65\u7D22\u5F15\u5931\u8D25:", errorMessage);
|
|
92
|
+
this.broadcastIndexProgress({
|
|
93
|
+
indexed: 0,
|
|
94
|
+
total: 0,
|
|
95
|
+
stage: "error",
|
|
96
|
+
error: errorMessage
|
|
97
|
+
});
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:status`, async () => {
|
|
102
|
+
return {
|
|
103
|
+
isIndexing: this.isIndexing,
|
|
104
|
+
lastProgress: this.lastProgress
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:cancel`, async () => {
|
|
108
|
+
try {
|
|
109
|
+
const searchPlugin = getSearchPlugin();
|
|
110
|
+
if (!searchPlugin) {
|
|
111
|
+
return { success: false, message: "\u641C\u7D22\u63D2\u4EF6\u672A\u521D\u59CB\u5316" };
|
|
112
|
+
}
|
|
113
|
+
const cancelled = searchPlugin.search.cancelIndexing();
|
|
114
|
+
if (cancelled) {
|
|
115
|
+
this.broadcastIndexProgress({
|
|
116
|
+
indexed: this.lastProgress?.indexed || 0,
|
|
117
|
+
total: this.lastProgress?.total || 0,
|
|
118
|
+
stage: "cancelled"
|
|
119
|
+
});
|
|
120
|
+
this.isIndexing = false;
|
|
121
|
+
return { success: true };
|
|
122
|
+
} else {
|
|
123
|
+
return { success: false, message: "\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u7D22\u5F15\u4EFB\u52A1" };
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("[AI-Search] \u53D6\u6D88\u7D22\u5F15\u5931\u8D25:", error);
|
|
127
|
+
return { success: false, message: String(error) };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
this.ipcMain.handle(`${this.channelPrefix}:index:delete`, async () => {
|
|
131
|
+
try {
|
|
132
|
+
const searchPlugin = getSearchPlugin();
|
|
133
|
+
if (!searchPlugin) {
|
|
134
|
+
throw new Error("\u641C\u7D22\u63D2\u4EF6\u672A\u521D\u59CB\u5316");
|
|
135
|
+
}
|
|
136
|
+
await searchPlugin.search.clear();
|
|
137
|
+
this.isIndexing = false;
|
|
138
|
+
this.lastProgress = null;
|
|
139
|
+
this.broadcastIndexProgress({
|
|
140
|
+
indexed: 0,
|
|
141
|
+
total: 0,
|
|
142
|
+
stage: "done"
|
|
143
|
+
});
|
|
144
|
+
return { success: true };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error("[AI-Search] \u5220\u9664\u7D22\u5F15\u5931\u8D25:", error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** 广播索引进度到所有前端监听器 */
|
|
152
|
+
broadcastIndexProgress(progress) {
|
|
153
|
+
this.lastProgress = progress;
|
|
154
|
+
this.indexingListeners.forEach((webContents) => {
|
|
155
|
+
if (!webContents.isDestroyed()) {
|
|
156
|
+
webContents.send(`${this.channelPrefix}:index:progress`, {
|
|
157
|
+
indexed: progress.indexed,
|
|
158
|
+
total: progress.total,
|
|
159
|
+
currentFile: progress.currentFile,
|
|
160
|
+
stage: progress.stage
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
this.indexingListeners.delete(webContents);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function createSearchElectronBridge(options) {
|
|
169
|
+
return new SearchElectronBridge(options);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
SearchElectronBridge,
|
|
174
|
+
createSearchElectronBridge
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=chunk-GAT4F5NK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bridge/electron.ts"],"sourcesContent":["/**\n * Electron 桥接模块\n * \n * 使用全局进度监听器自动广播索引进度\n * 无需包装方法,无需轮询,解决时序问题\n */\n\nimport type { IpcMain, WebContents } from 'electron';\nimport { getSearchPlugin } from '../tools';\nimport { addGlobalProgressListener } from '../core/progress';\nimport type { IndexProgress } from '../types';\n\n// 扩展的索引进度类型,支持取消和错误状态\ntype ExtendedIndexProgress = IndexProgress | {\n indexed: number;\n total: number;\n currentFile?: string;\n stage: 'cancelled' | 'error';\n error?: string;\n};\n\n/** Electron 桥接选项 */\nexport interface SearchElectronBridgeOptions {\n /** IPC channel 前缀 */\n channelPrefix?: string;\n /** IPC Main 实例 */\n ipcMain: IpcMain;\n}\n\n/** 索引进度广播系统 */\nexport class SearchElectronBridge {\n private channelPrefix: string;\n private ipcMain: IpcMain;\n private indexingListeners = new Set<WebContents>();\n private lastProgress: ExtendedIndexProgress | null = null;\n private isIndexing = false;\n\n constructor(options: SearchElectronBridgeOptions) {\n this.channelPrefix = options.channelPrefix || 'ai-chat';\n this.ipcMain = options.ipcMain;\n }\n\n /** 初始化桥接(注册 IPC handlers 和全局进度监听器) */\n init(): void {\n this.registerIpcHandlers();\n this.registerGlobalProgressListener();\n console.log('[AI-Search] Electron 桥接已初始化');\n }\n\n /** 注册全局进度监听器 */\n private registerGlobalProgressListener(): void {\n // 注册全局进度监听器,所有 indexDirectory 调用都会触发\n addGlobalProgressListener((progress: IndexProgress) => {\n // 更新状态\n this.isIndexing = progress.stage !== 'done';\n this.lastProgress = progress;\n \n // 广播进度到所有前端监听器\n this.broadcastIndexProgress(progress);\n });\n }\n\n /** 注册 IPC handlers */\n private registerIpcHandlers(): void {\n // 注册索引进度监听器(前端调用)\n this.ipcMain.handle(`${this.channelPrefix}:index:registerListener`, (event) => {\n const webContents = event.sender;\n this.indexingListeners.add(webContents);\n \n // 如果正在索引,立即发送最后进度\n if (this.lastProgress && this.isIndexing) {\n if (!webContents.isDestroyed()) {\n webContents.send(`${this.channelPrefix}:index:progress`, {\n indexed: this.lastProgress.indexed,\n total: this.lastProgress.total,\n currentFile: this.lastProgress.currentFile,\n stage: this.lastProgress.stage,\n });\n }\n }\n \n return { success: true };\n });\n\n // 注销索引进度监听器(前端调用)\n this.ipcMain.handle(`${this.channelPrefix}:index:unregisterListener`, (event) => {\n this.indexingListeners.delete(event.sender);\n return { success: true };\n });\n\n // 获取索引统计信息\n this.ipcMain.handle(`${this.channelPrefix}:index:getStats`, async () => {\n try {\n const searchPlugin = getSearchPlugin();\n \n if (!searchPlugin) {\n return {\n totalDocuments: 0,\n indexSize: 0,\n lastUpdated: null,\n };\n }\n \n const stats = searchPlugin.search.getStats();\n return {\n totalDocuments: stats.totalDocuments,\n indexSize: stats.indexSize,\n lastUpdated: stats.lastUpdated ? stats.lastUpdated.toISOString() : null,\n };\n } catch (error) {\n console.error('[AI-Search] 获取索引统计失败:', error);\n return {\n totalDocuments: 0,\n indexSize: 0,\n lastUpdated: null,\n };\n }\n });\n\n // 同步索引(重新索引工作空间)\n this.ipcMain.handle(`${this.channelPrefix}:index:sync`, async () => {\n try {\n const searchPlugin = getSearchPlugin();\n \n if (!searchPlugin) {\n throw new Error('搜索插件未初始化');\n }\n\n const workspaceState = searchPlugin.getWorkspaceState();\n if (!workspaceState.directory) {\n throw new Error('工作空间未设置');\n }\n\n // 开始索引(进度会通过全局监听器自动广播)\n await searchPlugin.search.indexDirectory(workspaceState.directory!);\n \n return { success: true };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.error('[AI-Search] 同步索引失败:', errorMessage);\n \n // 广播错误状态\n this.broadcastIndexProgress({\n indexed: 0,\n total: 0,\n stage: 'error',\n error: errorMessage,\n });\n \n throw error;\n }\n });\n\n // 检查索引状态\n this.ipcMain.handle(`${this.channelPrefix}:index:status`, async () => {\n return {\n isIndexing: this.isIndexing,\n lastProgress: this.lastProgress,\n };\n });\n\n // 取消索引\n this.ipcMain.handle(`${this.channelPrefix}:index:cancel`, async () => {\n try {\n const searchPlugin = getSearchPlugin();\n \n if (!searchPlugin) {\n return { success: false, message: '搜索插件未初始化' };\n }\n\n const cancelled = searchPlugin.search.cancelIndexing();\n \n if (cancelled) {\n // 广播取消状态\n this.broadcastIndexProgress({\n indexed: this.lastProgress?.indexed || 0,\n total: this.lastProgress?.total || 0,\n stage: 'cancelled',\n });\n this.isIndexing = false;\n return { success: true };\n } else {\n return { success: false, message: '没有正在运行的索引任务' };\n }\n } catch (error) {\n console.error('[AI-Search] 取消索引失败:', error);\n return { success: false, message: String(error) };\n }\n });\n\n // 删除索引\n this.ipcMain.handle(`${this.channelPrefix}:index:delete`, async () => {\n try {\n const searchPlugin = getSearchPlugin();\n \n if (!searchPlugin) {\n throw new Error('搜索插件未初始化');\n }\n\n await searchPlugin.search.clear();\n \n // 重置状态\n this.isIndexing = false;\n this.lastProgress = null;\n \n // 广播清空状态(使用 done 阶段,indexed=0 表示已清空)\n this.broadcastIndexProgress({\n indexed: 0,\n total: 0,\n stage: 'done',\n });\n \n return { success: true };\n } catch (error) {\n console.error('[AI-Search] 删除索引失败:', error);\n throw error;\n }\n });\n }\n\n /** 广播索引进度到所有前端监听器 */\n private broadcastIndexProgress(progress: ExtendedIndexProgress): void {\n // 更新最后进度\n this.lastProgress = progress;\n \n // 向所有监听器广播\n this.indexingListeners.forEach((webContents) => {\n if (!webContents.isDestroyed()) {\n webContents.send(`${this.channelPrefix}:index:progress`, {\n indexed: progress.indexed,\n total: progress.total,\n currentFile: progress.currentFile,\n stage: progress.stage,\n });\n } else {\n // 清理已销毁的 webContents\n this.indexingListeners.delete(webContents);\n }\n });\n }\n}\n\n/**\n * 创建搜索 Electron 桥接\n * \n * @example\n * ```typescript\n * import { createSearchElectronBridge } from '@huyooo/ai-search/bridge/electron';\n * import { ipcMain } from 'electron';\n * \n * const bridge = createSearchElectronBridge({\n * ipcMain,\n * channelPrefix: 'ai-chat',\n * });\n * bridge.init();\n * ```\n */\nexport function createSearchElectronBridge(\n options: SearchElectronBridgeOptions\n): SearchElectronBridge {\n return new SearchElectronBridge(options);\n}\n"],"mappings":"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","names":[]}
|