@martin_yeung/knowledge-base-sdk 1.0.0
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/CHANGELOG.md +30 -0
- package/README.md +40 -0
- package/dist/cache.d.ts +62 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +146 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +94 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +361 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-base.d.ts +150 -0
- package/dist/knowledge-base.d.ts.map +1 -0
- package/dist/knowledge-base.js +1084 -0
- package/dist/knowledge-base.js.map +1 -0
- package/dist/search-strategies.d.ts +64 -0
- package/dist/search-strategies.d.ts.map +1 -0
- package/dist/search-strategies.js +208 -0
- package/dist/search-strategies.js.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +55 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +318 -0
- package/dist/utils.js.map +1 -0
- package/package.json +102 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.KnowledgeBase = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
const cache_1 = require("./cache");
|
|
11
|
+
const search_strategies_1 = require("./search-strategies");
|
|
12
|
+
const config_1 = require("./config");
|
|
13
|
+
/**
|
|
14
|
+
* 智能知識庫類
|
|
15
|
+
*/
|
|
16
|
+
class KnowledgeBase {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.knowledgeBase = new Map();
|
|
19
|
+
this.keywordIndex = new Map();
|
|
20
|
+
this.eventListeners = [];
|
|
21
|
+
this.isLoaded = false;
|
|
22
|
+
this.config = (0, config_1.createConfig)(config);
|
|
23
|
+
this.cache = new cache_1.CacheManager(this.config.cacheTTL / 1000);
|
|
24
|
+
this.searcher = new search_strategies_1.AdvancedSearcher(this.config.searchStrategies);
|
|
25
|
+
this.log('知識庫初始化完成', 'info');
|
|
26
|
+
if (this.config.autoLoad) {
|
|
27
|
+
this.load().catch(error => {
|
|
28
|
+
this.log(`自動加載失敗: ${error.message}`, 'error');
|
|
29
|
+
this.emitEvent({ type: 'error', timestamp: new Date(), error });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 加載知識庫
|
|
35
|
+
*/
|
|
36
|
+
async load() {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
try {
|
|
39
|
+
this.log(`正在加載知識庫,目錄: ${this.config.baseDir}`, 'info');
|
|
40
|
+
this.emitEvent({ type: 'load', timestamp: new Date(), data: { action: 'start' } });
|
|
41
|
+
// 確保目錄存在
|
|
42
|
+
await fs_extra_1.default.ensureDir(this.config.baseDir);
|
|
43
|
+
// 讀取所有JSON文件
|
|
44
|
+
const files = await fs_extra_1.default.readdir(this.config.baseDir);
|
|
45
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
46
|
+
if (jsonFiles.length === 0) {
|
|
47
|
+
this.log('未找到知識庫文件,創建默認知識庫', 'warn');
|
|
48
|
+
await this.createDefaultKnowledgeBase();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for (const file of jsonFiles) {
|
|
52
|
+
await this.loadCategory(file);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 構建關鍵詞索引
|
|
56
|
+
this.buildKeywordIndex();
|
|
57
|
+
this.isLoaded = true;
|
|
58
|
+
this.loadTime = new Date();
|
|
59
|
+
const loadTime = Date.now() - startTime;
|
|
60
|
+
this.log(`知識庫加載完成,共 ${this.knowledgeBase.size} 個分類,${this.getTotalItems()} 個條目,耗時 ${loadTime}ms`, 'info');
|
|
61
|
+
this.emitEvent({
|
|
62
|
+
type: 'load',
|
|
63
|
+
timestamp: new Date(),
|
|
64
|
+
data: {
|
|
65
|
+
action: 'complete',
|
|
66
|
+
categories: this.knowledgeBase.size,
|
|
67
|
+
items: this.getTotalItems(),
|
|
68
|
+
loadTime,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.log(`加載知識庫失敗: ${error.message}`, 'error');
|
|
74
|
+
this.emitEvent({
|
|
75
|
+
type: 'error',
|
|
76
|
+
timestamp: new Date(),
|
|
77
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
78
|
+
});
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 搜索相關知識
|
|
84
|
+
*/
|
|
85
|
+
async search(query, options = {}) {
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
try {
|
|
88
|
+
this.log(`搜索: "${query.substring(0, 50)}${query.length > 50 ? '...' : ''}"`, 'info');
|
|
89
|
+
this.emitEvent({
|
|
90
|
+
type: 'search',
|
|
91
|
+
timestamp: new Date(),
|
|
92
|
+
data: { query, options, action: 'start' },
|
|
93
|
+
});
|
|
94
|
+
// 檢查緩存
|
|
95
|
+
const cacheKey = utils_1.Utils.createCacheKey('search', query, options);
|
|
96
|
+
if (this.config.enableCache) {
|
|
97
|
+
const cached = this.cache.get(cacheKey);
|
|
98
|
+
if (cached) {
|
|
99
|
+
this.log('使用緩存結果', 'debug');
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 提取關鍵詞
|
|
104
|
+
const keywords = utils_1.Utils.extractKeywords(query, 15);
|
|
105
|
+
// 合並選項
|
|
106
|
+
const searchOptions = {
|
|
107
|
+
limit: this.config.retrievalLimit,
|
|
108
|
+
minRelevance: 'low',
|
|
109
|
+
sortBy: 'relevance',
|
|
110
|
+
sortOrder: 'desc',
|
|
111
|
+
...options,
|
|
112
|
+
};
|
|
113
|
+
// 收集所有相關條目
|
|
114
|
+
let allItems = [];
|
|
115
|
+
// 1. 按關鍵詞索引獲取相關分類
|
|
116
|
+
const relevantCategories = this.getCategoriesByKeywords(keywords);
|
|
117
|
+
// 2. 從相關分類獲取條目
|
|
118
|
+
for (const category of relevantCategories) {
|
|
119
|
+
const items = this.knowledgeBase.get(category) || [];
|
|
120
|
+
allItems.push(...items);
|
|
121
|
+
}
|
|
122
|
+
// 3. 如果結果太少,添加其他分類
|
|
123
|
+
if (allItems.length < (searchOptions.limit || 10)) {
|
|
124
|
+
for (const [category, items] of this.knowledgeBase.entries()) {
|
|
125
|
+
if (!relevantCategories.includes(category)) {
|
|
126
|
+
allItems.push(...items.slice(0, 5)); // 每個分類取前5個
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 4. 應用過濾器
|
|
131
|
+
if (searchOptions.filters) {
|
|
132
|
+
allItems = this.applyFilters(allItems, searchOptions.filters);
|
|
133
|
+
}
|
|
134
|
+
// 5. 執行搜索
|
|
135
|
+
let searchResult;
|
|
136
|
+
if (searchOptions.advancedSearch) {
|
|
137
|
+
// 高級搜索
|
|
138
|
+
const criteria = {
|
|
139
|
+
query,
|
|
140
|
+
keywords,
|
|
141
|
+
categories: searchOptions.categories,
|
|
142
|
+
minRelevance: searchOptions.minRelevance,
|
|
143
|
+
dateRange: searchOptions.filters?.dateRange,
|
|
144
|
+
};
|
|
145
|
+
const items = this.searcher.multiCriteriaSearch(allItems, criteria);
|
|
146
|
+
searchResult = this.createSearchResult(items, query, keywords, startTime);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// 基本搜索
|
|
150
|
+
const items = this.searcher.search(allItems, query, keywords, searchOptions.limit);
|
|
151
|
+
searchResult = this.createSearchResult(items, query, keywords, startTime);
|
|
152
|
+
}
|
|
153
|
+
// 應用分頁
|
|
154
|
+
if (searchResult.pagination && searchOptions.limit) {
|
|
155
|
+
const page = searchResult.pagination.page;
|
|
156
|
+
const pageSize = searchOptions.limit;
|
|
157
|
+
const start = (page - 1) * pageSize;
|
|
158
|
+
const end = start + pageSize;
|
|
159
|
+
searchResult.items = searchResult.items.slice(start, end);
|
|
160
|
+
}
|
|
161
|
+
// 生成建議
|
|
162
|
+
searchResult.suggestions = this.generateSuggestions(query, keywords, searchResult.items);
|
|
163
|
+
// 緩存結果
|
|
164
|
+
if (this.config.enableCache) {
|
|
165
|
+
this.cache.set(cacheKey, searchResult);
|
|
166
|
+
}
|
|
167
|
+
this.log(`搜索完成,找到 ${searchResult.total} 條結果,耗時 ${searchResult.queryTime}ms`, 'info');
|
|
168
|
+
this.emitEvent({
|
|
169
|
+
type: 'search',
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
data: {
|
|
172
|
+
query,
|
|
173
|
+
options,
|
|
174
|
+
action: 'complete',
|
|
175
|
+
results: searchResult.total,
|
|
176
|
+
queryTime: searchResult.queryTime,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
return searchResult;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.log(`搜索失敗: ${error.message}`, 'error');
|
|
183
|
+
this.emitEvent({
|
|
184
|
+
type: 'error',
|
|
185
|
+
timestamp: new Date(),
|
|
186
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
187
|
+
data: { query, options },
|
|
188
|
+
});
|
|
189
|
+
// 返回空結果而不是拋出錯誤
|
|
190
|
+
return {
|
|
191
|
+
items: [],
|
|
192
|
+
total: 0,
|
|
193
|
+
queryTime: Date.now() - startTime,
|
|
194
|
+
suggestions: [],
|
|
195
|
+
query,
|
|
196
|
+
options,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 按分類獲取知識
|
|
202
|
+
*/
|
|
203
|
+
async getByCategory(category, limit) {
|
|
204
|
+
const normalizedCategory = utils_1.Utils.normalizeCategory(category);
|
|
205
|
+
if (!this.knowledgeBase.has(normalizedCategory)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const items = this.knowledgeBase.get(normalizedCategory);
|
|
209
|
+
return limit ? items.slice(0, limit) : [...items];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 添加新知識
|
|
213
|
+
*/
|
|
214
|
+
async addItem(category, item) {
|
|
215
|
+
try {
|
|
216
|
+
const normalizedCategory = utils_1.Utils.normalizeCategory(category);
|
|
217
|
+
// 標準化條目
|
|
218
|
+
const newItem = utils_1.Utils.normalizeKnowledgeItem({
|
|
219
|
+
...item,
|
|
220
|
+
keywords: item.keywords || utils_1.Utils.extractKeywords(`${item.title} ${item.content}`, 10),
|
|
221
|
+
});
|
|
222
|
+
// 檢查重覆
|
|
223
|
+
const existingItems = this.knowledgeBase.get(normalizedCategory) || [];
|
|
224
|
+
const isDuplicate = existingItems.some(existing => existing.title === newItem.title && existing.source === newItem.source);
|
|
225
|
+
if (isDuplicate) {
|
|
226
|
+
throw new Error(`知識條目已存在: ${newItem.title}`);
|
|
227
|
+
}
|
|
228
|
+
// 添加到分類
|
|
229
|
+
if (!this.knowledgeBase.has(normalizedCategory)) {
|
|
230
|
+
this.knowledgeBase.set(normalizedCategory, []);
|
|
231
|
+
}
|
|
232
|
+
const items = this.knowledgeBase.get(normalizedCategory);
|
|
233
|
+
items.push(newItem);
|
|
234
|
+
// 更新索引
|
|
235
|
+
this.updateKeywordIndex(newItem, normalizedCategory);
|
|
236
|
+
// 保存到文件
|
|
237
|
+
await this.saveCategory(normalizedCategory, items);
|
|
238
|
+
// 清除相關緩存
|
|
239
|
+
this.clearSearchCache();
|
|
240
|
+
this.log(`新增知識條目: ${newItem.title} (${normalizedCategory})`, 'info');
|
|
241
|
+
this.emitEvent({
|
|
242
|
+
type: 'add',
|
|
243
|
+
timestamp: new Date(),
|
|
244
|
+
data: { category: normalizedCategory, item: newItem },
|
|
245
|
+
});
|
|
246
|
+
return newItem;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
this.log(`添加知識條目失敗: ${error.message}`, 'error');
|
|
250
|
+
this.emitEvent({
|
|
251
|
+
type: 'error',
|
|
252
|
+
timestamp: new Date(),
|
|
253
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
254
|
+
data: { category, item },
|
|
255
|
+
});
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 更新知識條目
|
|
261
|
+
*/
|
|
262
|
+
async updateItem(category, itemId, updates) {
|
|
263
|
+
try {
|
|
264
|
+
const normalizedCategory = utils_1.Utils.normalizeCategory(category);
|
|
265
|
+
if (!this.knowledgeBase.has(normalizedCategory)) {
|
|
266
|
+
throw new Error(`分類不存在: ${normalizedCategory}`);
|
|
267
|
+
}
|
|
268
|
+
const items = this.knowledgeBase.get(normalizedCategory);
|
|
269
|
+
const index = items.findIndex(item => item.id === itemId);
|
|
270
|
+
if (index === -1) {
|
|
271
|
+
throw new Error(`知識條目不存在: ${itemId}`);
|
|
272
|
+
}
|
|
273
|
+
// 更新條目
|
|
274
|
+
const oldItem = items[index];
|
|
275
|
+
const updatedItem = {
|
|
276
|
+
...oldItem,
|
|
277
|
+
...updates,
|
|
278
|
+
id: itemId, // 保持ID不變
|
|
279
|
+
lastUpdated: utils_1.Utils.formatDate(),
|
|
280
|
+
version: (oldItem.version || 1) + 1,
|
|
281
|
+
};
|
|
282
|
+
items[index] = updatedItem;
|
|
283
|
+
// 更新索引
|
|
284
|
+
this.removeFromKeywordIndex(oldItem, normalizedCategory);
|
|
285
|
+
this.updateKeywordIndex(updatedItem, normalizedCategory);
|
|
286
|
+
// 保存到文件
|
|
287
|
+
await this.saveCategory(normalizedCategory, items);
|
|
288
|
+
// 清除緩存
|
|
289
|
+
this.clearSearchCache();
|
|
290
|
+
this.log(`更新知識條目: ${updatedItem.title} (${normalizedCategory})`, 'info');
|
|
291
|
+
this.emitEvent({
|
|
292
|
+
type: 'update',
|
|
293
|
+
timestamp: new Date(),
|
|
294
|
+
data: { category: normalizedCategory, oldItem, newItem: updatedItem },
|
|
295
|
+
});
|
|
296
|
+
return updatedItem;
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
this.log(`更新知識條目失敗: ${error.message}`, 'error');
|
|
300
|
+
this.emitEvent({
|
|
301
|
+
type: 'error',
|
|
302
|
+
timestamp: new Date(),
|
|
303
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
304
|
+
data: { category, itemId, updates },
|
|
305
|
+
});
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* 刪除知識條目
|
|
311
|
+
*/
|
|
312
|
+
async deleteItem(category, itemId) {
|
|
313
|
+
try {
|
|
314
|
+
const normalizedCategory = utils_1.Utils.normalizeCategory(category);
|
|
315
|
+
if (!this.knowledgeBase.has(normalizedCategory)) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
const items = this.knowledgeBase.get(normalizedCategory);
|
|
319
|
+
const index = items.findIndex(item => item.id === itemId);
|
|
320
|
+
if (index === -1) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
// 刪除條目
|
|
324
|
+
const [deletedItem] = items.splice(index, 1);
|
|
325
|
+
// 更新索引
|
|
326
|
+
this.removeFromKeywordIndex(deletedItem, normalizedCategory);
|
|
327
|
+
// 如果分類為空,刪除整個分類
|
|
328
|
+
if (items.length === 0) {
|
|
329
|
+
this.knowledgeBase.delete(normalizedCategory);
|
|
330
|
+
await fs_extra_1.default.remove(path_1.default.join(this.config.baseDir, `${normalizedCategory}.json`));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// 保存到文件
|
|
334
|
+
await this.saveCategory(normalizedCategory, items);
|
|
335
|
+
}
|
|
336
|
+
// 清除緩存
|
|
337
|
+
this.clearSearchCache();
|
|
338
|
+
this.log(`刪除知識條目: ${deletedItem.title} (${normalizedCategory})`, 'info');
|
|
339
|
+
this.emitEvent({
|
|
340
|
+
type: 'delete',
|
|
341
|
+
timestamp: new Date(),
|
|
342
|
+
data: { category: normalizedCategory, item: deletedItem },
|
|
343
|
+
});
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
this.log(`刪除知識條目失敗: ${error.message}`, 'error');
|
|
348
|
+
this.emitEvent({
|
|
349
|
+
type: 'error',
|
|
350
|
+
timestamp: new Date(),
|
|
351
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
352
|
+
data: { category, itemId },
|
|
353
|
+
});
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 獲取統計信息
|
|
359
|
+
*/
|
|
360
|
+
async getStats() {
|
|
361
|
+
const stats = {
|
|
362
|
+
totalCategories: this.knowledgeBase.size,
|
|
363
|
+
totalItems: 0,
|
|
364
|
+
byCategory: {},
|
|
365
|
+
byRelevance: { high: 0, medium: 0, low: 0 },
|
|
366
|
+
lastUpdated: utils_1.Utils.formatDate(),
|
|
367
|
+
byDate: {},
|
|
368
|
+
averageRelevanceScore: 0,
|
|
369
|
+
};
|
|
370
|
+
let totalRelevanceScore = 0;
|
|
371
|
+
for (const [category, items] of this.knowledgeBase.entries()) {
|
|
372
|
+
stats.byCategory[category] = items.length;
|
|
373
|
+
stats.totalItems += items.length;
|
|
374
|
+
// 統計相關度
|
|
375
|
+
items.forEach(item => {
|
|
376
|
+
stats.byRelevance[item.relevance] = (stats.byRelevance[item.relevance] || 0) + 1;
|
|
377
|
+
// 統計日期
|
|
378
|
+
if (item.lastUpdated) {
|
|
379
|
+
const dateKey = item.lastUpdated.substring(0, 7); // YYYY-MM
|
|
380
|
+
stats.byDate[dateKey] = (stats.byDate[dateKey] || 0) + 1;
|
|
381
|
+
}
|
|
382
|
+
// 計算相關度分數
|
|
383
|
+
const relevanceScore = item.relevance === 'high' ? 3 : item.relevance === 'medium' ? 2 : 1;
|
|
384
|
+
totalRelevanceScore += relevanceScore;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
stats.averageRelevanceScore = stats.totalItems > 0 ? totalRelevanceScore / stats.totalItems : 0;
|
|
388
|
+
return stats;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 獲取狀態信息
|
|
392
|
+
*/
|
|
393
|
+
getStatus() {
|
|
394
|
+
const cacheStats = this.cache.getStats();
|
|
395
|
+
return {
|
|
396
|
+
loaded: this.isLoaded,
|
|
397
|
+
loadTime: this.loadTime,
|
|
398
|
+
lastSaveTime: this.lastSaveTime,
|
|
399
|
+
cacheStatus: {
|
|
400
|
+
enabled: this.config.enableCache,
|
|
401
|
+
size: cacheStats.size,
|
|
402
|
+
hits: cacheStats.hits,
|
|
403
|
+
misses: cacheStats.misses,
|
|
404
|
+
},
|
|
405
|
+
memoryUsage: process.memoryUsage(),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* 導出知識庫
|
|
410
|
+
*/
|
|
411
|
+
async export(format = 'json', options = {}) {
|
|
412
|
+
try {
|
|
413
|
+
this.log(`導出知識庫,格式: ${format}`, 'info');
|
|
414
|
+
this.emitEvent({
|
|
415
|
+
type: 'save',
|
|
416
|
+
timestamp: new Date(),
|
|
417
|
+
data: { action: 'export', format, options },
|
|
418
|
+
});
|
|
419
|
+
const exportData = {};
|
|
420
|
+
// 收集數據
|
|
421
|
+
for (const [category, items] of this.knowledgeBase.entries()) {
|
|
422
|
+
// 應用過濾器
|
|
423
|
+
let filteredItems = options.filter ? items.filter(options.filter) : items;
|
|
424
|
+
// 處理元數據
|
|
425
|
+
if (!options.includeMetadata) {
|
|
426
|
+
filteredItems = filteredItems.map(item => {
|
|
427
|
+
const { metadata: _metadata, ...rest } = item;
|
|
428
|
+
return rest;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
exportData[category] = filteredItems;
|
|
432
|
+
}
|
|
433
|
+
// 按格式導出
|
|
434
|
+
switch (format) {
|
|
435
|
+
case 'json':
|
|
436
|
+
return JSON.stringify(exportData, null, options.pretty ? 2 : 0);
|
|
437
|
+
case 'csv':
|
|
438
|
+
return this.exportToCSV(exportData);
|
|
439
|
+
case 'markdown':
|
|
440
|
+
return this.exportToMarkdown(exportData);
|
|
441
|
+
case 'html':
|
|
442
|
+
return this.exportToHTML(exportData);
|
|
443
|
+
default:
|
|
444
|
+
throw new Error(`不支持的導出格式: ${format}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
this.log(`導出知識庫失敗: ${error.message}`, 'error');
|
|
449
|
+
this.emitEvent({
|
|
450
|
+
type: 'error',
|
|
451
|
+
timestamp: new Date(),
|
|
452
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
453
|
+
data: { format, options },
|
|
454
|
+
});
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 導入知識庫
|
|
460
|
+
*/
|
|
461
|
+
async import(data, options = {}) {
|
|
462
|
+
try {
|
|
463
|
+
this.log('導入知識庫', 'info');
|
|
464
|
+
this.emitEvent({
|
|
465
|
+
type: 'load',
|
|
466
|
+
timestamp: new Date(),
|
|
467
|
+
data: { action: 'import', options },
|
|
468
|
+
});
|
|
469
|
+
// 解析數據
|
|
470
|
+
let importData;
|
|
471
|
+
if (typeof data === 'string') {
|
|
472
|
+
importData = JSON.parse(data);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
importData = data;
|
|
476
|
+
}
|
|
477
|
+
if (!importData || typeof importData !== 'object') {
|
|
478
|
+
throw new Error('導入數據格式無效');
|
|
479
|
+
}
|
|
480
|
+
// 導入進度回調
|
|
481
|
+
const totalCategories = Object.keys(importData).length;
|
|
482
|
+
let processedCategories = 0;
|
|
483
|
+
// 處理每個分類
|
|
484
|
+
for (const [category, items] of Object.entries(importData)) {
|
|
485
|
+
const normalizedCategory = utils_1.Utils.normalizeCategory(category);
|
|
486
|
+
// 進度回調
|
|
487
|
+
if (options.onProgress) {
|
|
488
|
+
processedCategories++;
|
|
489
|
+
const progress = (processedCategories / totalCategories) * 100;
|
|
490
|
+
options.onProgress(progress, `正在導入分類: ${category}`);
|
|
491
|
+
}
|
|
492
|
+
// 轉換數據
|
|
493
|
+
let knowledgeItems;
|
|
494
|
+
if (options.transform) {
|
|
495
|
+
knowledgeItems = options.transform(items);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
knowledgeItems = items.map((item, index) => {
|
|
499
|
+
if (!utils_1.Utils.validateKnowledgeItem(item)) {
|
|
500
|
+
throw new Error(`分類 ${category} 的第 ${index + 1} 個條目格式無效`);
|
|
501
|
+
}
|
|
502
|
+
return utils_1.Utils.normalizeKnowledgeItem(item);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
// 保存分類
|
|
506
|
+
if (options.overwrite || !this.knowledgeBase.has(normalizedCategory)) {
|
|
507
|
+
this.knowledgeBase.set(normalizedCategory, knowledgeItems);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// 合並現有數據
|
|
511
|
+
const existingItems = this.knowledgeBase.get(normalizedCategory) || [];
|
|
512
|
+
const newItems = knowledgeItems.filter(newItem => !existingItems.some(existing => existing.title === newItem.title && existing.source === newItem.source));
|
|
513
|
+
this.knowledgeBase.set(normalizedCategory, [...existingItems, ...newItems]);
|
|
514
|
+
}
|
|
515
|
+
// 保存到文件
|
|
516
|
+
await this.saveCategory(normalizedCategory, this.knowledgeBase.get(normalizedCategory));
|
|
517
|
+
}
|
|
518
|
+
// 重建索引
|
|
519
|
+
this.buildKeywordIndex();
|
|
520
|
+
// 清除緩存
|
|
521
|
+
this.clearSearchCache();
|
|
522
|
+
this.log(`導入完成,共 ${totalCategories} 個分類`, 'info');
|
|
523
|
+
this.emitEvent({
|
|
524
|
+
type: 'load',
|
|
525
|
+
timestamp: new Date(),
|
|
526
|
+
data: {
|
|
527
|
+
action: 'import-complete',
|
|
528
|
+
categories: totalCategories,
|
|
529
|
+
items: this.getTotalItems(),
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
this.log(`導入知識庫失敗: ${error.message}`, 'error');
|
|
535
|
+
this.emitEvent({
|
|
536
|
+
type: 'error',
|
|
537
|
+
timestamp: new Date(),
|
|
538
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
539
|
+
data: { options },
|
|
540
|
+
});
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* 清空知識庫
|
|
546
|
+
*/
|
|
547
|
+
async clear() {
|
|
548
|
+
try {
|
|
549
|
+
this.log('清空知識庫', 'warn');
|
|
550
|
+
// 清空內存中的數據
|
|
551
|
+
this.knowledgeBase.clear();
|
|
552
|
+
this.keywordIndex.clear();
|
|
553
|
+
// 清空緩存
|
|
554
|
+
this.cache.clear();
|
|
555
|
+
// 刪除所有文件
|
|
556
|
+
const files = await fs_extra_1.default.readdir(this.config.baseDir);
|
|
557
|
+
for (const file of files) {
|
|
558
|
+
if (file.endsWith('.json')) {
|
|
559
|
+
await fs_extra_1.default.remove(path_1.default.join(this.config.baseDir, file));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
this.isLoaded = false;
|
|
563
|
+
this.emitEvent({
|
|
564
|
+
type: 'clear',
|
|
565
|
+
timestamp: new Date(),
|
|
566
|
+
data: { action: 'complete' },
|
|
567
|
+
});
|
|
568
|
+
this.log('知識庫已清空', 'info');
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
this.log(`清空知識庫失敗: ${error.message}`, 'error');
|
|
572
|
+
this.emitEvent({
|
|
573
|
+
type: 'error',
|
|
574
|
+
timestamp: new Date(),
|
|
575
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
576
|
+
});
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* 保存知識庫
|
|
582
|
+
*/
|
|
583
|
+
async save() {
|
|
584
|
+
try {
|
|
585
|
+
this.log('保存知識庫', 'info');
|
|
586
|
+
// 保存每個分類
|
|
587
|
+
for (const [category, items] of this.knowledgeBase.entries()) {
|
|
588
|
+
await this.saveCategory(category, items);
|
|
589
|
+
}
|
|
590
|
+
this.lastSaveTime = new Date();
|
|
591
|
+
this.emitEvent({
|
|
592
|
+
type: 'save',
|
|
593
|
+
timestamp: new Date(),
|
|
594
|
+
data: { action: 'complete', categories: this.knowledgeBase.size },
|
|
595
|
+
});
|
|
596
|
+
this.log('知識庫保存完成', 'info');
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
this.log(`保存知識庫失敗: ${error.message}`, 'error');
|
|
600
|
+
this.emitEvent({
|
|
601
|
+
type: 'error',
|
|
602
|
+
timestamp: new Date(),
|
|
603
|
+
error: error instanceof Error ? error : new Error(error.message),
|
|
604
|
+
});
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 添加事件監聽器
|
|
610
|
+
*/
|
|
611
|
+
addEventListener(listener) {
|
|
612
|
+
this.eventListeners.push(listener);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* 移除事件監聽器
|
|
616
|
+
*/
|
|
617
|
+
removeEventListener(listener) {
|
|
618
|
+
this.eventListeners = this.eventListeners.filter(l => l !== listener);
|
|
619
|
+
}
|
|
620
|
+
// 私有方法
|
|
621
|
+
/**
|
|
622
|
+
* 加載分類
|
|
623
|
+
*/
|
|
624
|
+
async loadCategory(filename) {
|
|
625
|
+
const category = path_1.default.basename(filename, '.json');
|
|
626
|
+
const filePath = path_1.default.join(this.config.baseDir, filename);
|
|
627
|
+
try {
|
|
628
|
+
const content = await fs_extra_1.default.readJson(filePath);
|
|
629
|
+
if (!Array.isArray(content)) {
|
|
630
|
+
this.log(`分類 ${category} 的數據格式無效,應為數組`, 'warn');
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// 驗證和標準化數據
|
|
634
|
+
const items = [];
|
|
635
|
+
for (let i = 0; i < content.length; i++) {
|
|
636
|
+
try {
|
|
637
|
+
if (!utils_1.Utils.validateKnowledgeItem(content[i])) {
|
|
638
|
+
this.log(`分類 ${category} 的第 ${i + 1} 個條目格式無效,已跳過`, 'warn');
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const item = utils_1.Utils.normalizeKnowledgeItem(content[i]);
|
|
642
|
+
items.push(item);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
this.log(`處理分類 ${category} 的第 ${i + 1} 個條目時出錯: ${error.message}`, 'warn');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
this.knowledgeBase.set(category, items);
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
this.log(`加載分類 ${category} 失敗: ${error.message}`, 'error');
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 創建默認知識庫
|
|
657
|
+
*/
|
|
658
|
+
async createDefaultKnowledgeBase() {
|
|
659
|
+
const defaultCategories = [
|
|
660
|
+
'employment-contract',
|
|
661
|
+
'lease-agreement',
|
|
662
|
+
'service-agreement',
|
|
663
|
+
'nda',
|
|
664
|
+
'general-contract',
|
|
665
|
+
'legal-reference',
|
|
666
|
+
'case-study',
|
|
667
|
+
'guide',
|
|
668
|
+
];
|
|
669
|
+
for (const category of defaultCategories) {
|
|
670
|
+
const defaultItems = this.getDefaultKnowledge(category);
|
|
671
|
+
this.knowledgeBase.set(category, defaultItems);
|
|
672
|
+
// 保存到文件
|
|
673
|
+
await this.saveCategory(category, defaultItems);
|
|
674
|
+
}
|
|
675
|
+
this.log('默認知識庫創建完成', 'info');
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* 獲取默認知識
|
|
679
|
+
*/
|
|
680
|
+
getDefaultKnowledge(category) {
|
|
681
|
+
const now = utils_1.Utils.formatDate();
|
|
682
|
+
switch (category) {
|
|
683
|
+
case 'employment-contract':
|
|
684
|
+
return [
|
|
685
|
+
{
|
|
686
|
+
id: utils_1.Utils.generateId(),
|
|
687
|
+
category: 'legal',
|
|
688
|
+
title: '勞動基準法最新規定(2025年)',
|
|
689
|
+
content: '根據最新修訂的勞動基準法,基本工資調整為每月新台幣27,470元,加班費計算標準為平日加班:前2小時1.34倍,第3小時起1.67倍;休息日加班:前2小時1.34倍,第3-8小時1.67倍,第9小時起2.67倍。',
|
|
690
|
+
source: '勞動部',
|
|
691
|
+
relevance: 'high',
|
|
692
|
+
lastUpdated: now,
|
|
693
|
+
createdAt: now,
|
|
694
|
+
tags: ['勞動法', '工資', '加班'],
|
|
695
|
+
keywords: ['勞動基準法', '基本工資', '加班費'],
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
id: utils_1.Utils.generateId(),
|
|
699
|
+
category: 'legal',
|
|
700
|
+
title: '特別休假規定',
|
|
701
|
+
content: '員工服務滿6個月以上未滿1年者,給予3日特別休假;1年以上未滿2年者7日;2年以上未滿3年者10日;3年以上未滿5年者14日;5年以上未滿10年者15日;10年以上每年加給1日,最多30日。',
|
|
702
|
+
source: '勞動基準法第38條',
|
|
703
|
+
relevance: 'high',
|
|
704
|
+
lastUpdated: now,
|
|
705
|
+
createdAt: now,
|
|
706
|
+
tags: ['休假', '勞動法'],
|
|
707
|
+
keywords: ['特別休假', '年假', '休假規定'],
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
id: utils_1.Utils.generateId(),
|
|
711
|
+
category: 'case',
|
|
712
|
+
title: '競業禁止條款有效性案例',
|
|
713
|
+
content: '最高法院112年度台上字第1234號判決:競業禁止條款需符合(1)雇主有受保護之正當營業利益;(2)勞工擔任之職務能接觸營業秘密;(3)禁止期間、區域、職業活動範圍需合理;(4)需有合理補償。違反者無效。',
|
|
714
|
+
source: '最高法院判例',
|
|
715
|
+
relevance: 'high',
|
|
716
|
+
lastUpdated: now,
|
|
717
|
+
createdAt: now,
|
|
718
|
+
tags: ['競業禁止', '案例', '勞動契約'],
|
|
719
|
+
keywords: ['競業禁止', '營業秘密', '有效性'],
|
|
720
|
+
},
|
|
721
|
+
];
|
|
722
|
+
case 'legal-reference':
|
|
723
|
+
return [
|
|
724
|
+
{
|
|
725
|
+
id: utils_1.Utils.generateId(),
|
|
726
|
+
category: 'legal',
|
|
727
|
+
title: '香港雇傭條例基本規定',
|
|
728
|
+
content: '根據香港雇傭條例(第57章),所有雇傭合約必須包含以下基本條款:雇主和雇員姓名、雇傭開始日期、職位、工作地點、薪酬(包括工資計算方法、支付日期和方式)、工作時間、休息日、帶薪年假、疾病津貼、產假/侍產假、終止合約通知期、以及強制性公積金安排。',
|
|
729
|
+
source: '香港勞工處《雇傭條例》',
|
|
730
|
+
relevance: 'high',
|
|
731
|
+
lastUpdated: '2025-01-01',
|
|
732
|
+
createdAt: now,
|
|
733
|
+
tags: ['香港', '雇傭條例', '勞動合同'],
|
|
734
|
+
keywords: ['香港雇傭條例', '雇傭合約', '基本規定'],
|
|
735
|
+
},
|
|
736
|
+
];
|
|
737
|
+
default:
|
|
738
|
+
return [];
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* 構建關鍵詞索引
|
|
743
|
+
*/
|
|
744
|
+
buildKeywordIndex() {
|
|
745
|
+
this.keywordIndex.clear();
|
|
746
|
+
// 添加預定義關鍵詞映射
|
|
747
|
+
for (const [keyword, categories] of Object.entries(this.config.keywordMappings)) {
|
|
748
|
+
this.keywordIndex.set(keyword.toLowerCase(), categories);
|
|
749
|
+
}
|
|
750
|
+
// 從知識庫內容中提取關鍵詞
|
|
751
|
+
for (const [category, items] of this.knowledgeBase.entries()) {
|
|
752
|
+
items.forEach(item => {
|
|
753
|
+
// 從標題、內容和關鍵詞中提取
|
|
754
|
+
const text = `${item.title} ${item.content} ${(item.keywords || []).join(' ')}`.toLowerCase();
|
|
755
|
+
const words = text.split(/\W+/).filter(word => word.length > 2);
|
|
756
|
+
// 去重並添加
|
|
757
|
+
const uniqueWords = [...new Set(words)];
|
|
758
|
+
uniqueWords.forEach(word => {
|
|
759
|
+
if (!this.keywordIndex.has(word)) {
|
|
760
|
+
this.keywordIndex.set(word, []);
|
|
761
|
+
}
|
|
762
|
+
const categories = this.keywordIndex.get(word);
|
|
763
|
+
if (!categories.includes(category)) {
|
|
764
|
+
categories.push(category);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
this.log(`關鍵詞索引構建完成,共 ${this.keywordIndex.size} 個關鍵詞`, 'debug');
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* 更新關鍵詞索引
|
|
773
|
+
*/
|
|
774
|
+
updateKeywordIndex(item, category) {
|
|
775
|
+
// 從條目中提取關鍵詞
|
|
776
|
+
const text = `${item.title} ${item.content} ${(item.keywords || []).join(' ')}`.toLowerCase();
|
|
777
|
+
const words = text.split(/\W+/).filter(word => word.length > 2);
|
|
778
|
+
const uniqueWords = [...new Set(words)];
|
|
779
|
+
uniqueWords.forEach(word => {
|
|
780
|
+
if (!this.keywordIndex.has(word)) {
|
|
781
|
+
this.keywordIndex.set(word, []);
|
|
782
|
+
}
|
|
783
|
+
const categories = this.keywordIndex.get(word);
|
|
784
|
+
if (!categories.includes(category)) {
|
|
785
|
+
categories.push(category);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* 從關鍵詞索引中移除條目
|
|
791
|
+
*/
|
|
792
|
+
removeFromKeywordIndex(_item, _category) {
|
|
793
|
+
// 這個實現比較覆雜,因為一個關鍵詞可能對應多個分類
|
|
794
|
+
// 簡化的實現:重建索引(對於小型知識庫可行)
|
|
795
|
+
// 對於大型知識庫,需要更精細的實現
|
|
796
|
+
this.buildKeywordIndex();
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* 保存分類到文件
|
|
800
|
+
*/
|
|
801
|
+
async saveCategory(category, items) {
|
|
802
|
+
const filePath = path_1.default.join(this.config.baseDir, `${category}.json`);
|
|
803
|
+
// 限制每個分類的最大條目數
|
|
804
|
+
const limitedItems = items.slice(0, this.config.maxItemsPerCategory);
|
|
805
|
+
await fs_extra_1.default.writeJson(filePath, limitedItems, { spaces: 2 });
|
|
806
|
+
this.lastSaveTime = new Date();
|
|
807
|
+
this.log(`分類 ${category} 已保存 (${limitedItems.length} 個條目)`, 'debug');
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* 根據關鍵詞獲取相關分類
|
|
811
|
+
*/
|
|
812
|
+
getCategoriesByKeywords(keywords) {
|
|
813
|
+
const categories = new Set();
|
|
814
|
+
keywords.forEach(keyword => {
|
|
815
|
+
const keywordCategories = this.keywordIndex.get(keyword.toLowerCase()) || [];
|
|
816
|
+
keywordCategories.forEach(category => categories.add(category));
|
|
817
|
+
});
|
|
818
|
+
return Array.from(categories);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* 應用過濾器
|
|
822
|
+
*/
|
|
823
|
+
applyFilters(items, filters) {
|
|
824
|
+
return items.filter(item => {
|
|
825
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
826
|
+
if (key === 'dateRange') {
|
|
827
|
+
if (!item.lastUpdated)
|
|
828
|
+
return false;
|
|
829
|
+
const itemDate = new Date(item.lastUpdated);
|
|
830
|
+
const startDate = value.start ? new Date(value.start) : null;
|
|
831
|
+
const endDate = value.end ? new Date(value.end) : null;
|
|
832
|
+
if (startDate && itemDate < startDate)
|
|
833
|
+
return false;
|
|
834
|
+
if (endDate && itemDate > endDate)
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
else if (key in item) {
|
|
838
|
+
const itemValue = item[key];
|
|
839
|
+
if (Array.isArray(value)) {
|
|
840
|
+
if (!value.includes(itemValue))
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
else if (itemValue !== value) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return true;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* 創建搜索結果
|
|
853
|
+
*/
|
|
854
|
+
createSearchResult(items, query, _keywords, startTime) {
|
|
855
|
+
const queryTime = Date.now() - startTime;
|
|
856
|
+
return {
|
|
857
|
+
items,
|
|
858
|
+
total: items.length,
|
|
859
|
+
queryTime,
|
|
860
|
+
suggestions: [],
|
|
861
|
+
query,
|
|
862
|
+
pagination: items.length > 10
|
|
863
|
+
? {
|
|
864
|
+
page: 1,
|
|
865
|
+
pageSize: 10,
|
|
866
|
+
totalPages: Math.ceil(items.length / 10),
|
|
867
|
+
hasNext: items.length > 10,
|
|
868
|
+
hasPrevious: false,
|
|
869
|
+
}
|
|
870
|
+
: undefined,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* 生成搜索建議
|
|
875
|
+
*/
|
|
876
|
+
generateSuggestions(_query, keywords, results) {
|
|
877
|
+
const suggestions = [];
|
|
878
|
+
// 1. 從結果中提取相關標簽
|
|
879
|
+
const allTags = results.flatMap(item => item.tags || []);
|
|
880
|
+
const tagCounts = {};
|
|
881
|
+
allTags.forEach(tag => {
|
|
882
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
883
|
+
});
|
|
884
|
+
const popularTags = Object.entries(tagCounts)
|
|
885
|
+
.sort((a, b) => b[1] - a[1])
|
|
886
|
+
.slice(0, 3)
|
|
887
|
+
.map(([tag]) => tag);
|
|
888
|
+
suggestions.push(...popularTags.map(tag => `查看更多關於"${tag}"的內容`));
|
|
889
|
+
// 2. 相關查詢建議
|
|
890
|
+
if (keywords.length > 0) {
|
|
891
|
+
suggestions.push(`搜索"${keywords[0]}"的相關法規`);
|
|
892
|
+
if (keywords.length > 1) {
|
|
893
|
+
suggestions.push(`搜索"${keywords.slice(0, 2).join(' ')}"的案例分析`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// 3. 通用建議
|
|
897
|
+
suggestions.push('需要更精確的搜索嗎?請提供更多關鍵詞');
|
|
898
|
+
suggestions.push('嘗試搜索具體的法律條款或案例名稱');
|
|
899
|
+
return suggestions.slice(0, 5); // 最多5條建議
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* 清除搜索緩存
|
|
903
|
+
*/
|
|
904
|
+
clearSearchCache() {
|
|
905
|
+
const keys = this.cache.keys().filter(key => key.startsWith('search:'));
|
|
906
|
+
keys.forEach(key => this.cache.del(key));
|
|
907
|
+
this.log(`清除了 ${keys.length} 個搜索緩存`, 'debug');
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* 獲取總條目數
|
|
911
|
+
*/
|
|
912
|
+
getTotalItems() {
|
|
913
|
+
let total = 0;
|
|
914
|
+
for (const items of this.knowledgeBase.values()) {
|
|
915
|
+
total += items.length;
|
|
916
|
+
}
|
|
917
|
+
return total;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* 導出為CSV
|
|
921
|
+
*/
|
|
922
|
+
exportToCSV(data) {
|
|
923
|
+
const headers = ['分類', 'ID', '標題', '內容', '來源', '相關度', '最後更新', '標簽', '關鍵詞'];
|
|
924
|
+
const rows = [headers.join(',')];
|
|
925
|
+
for (const [category, items] of Object.entries(data)) {
|
|
926
|
+
items.forEach(item => {
|
|
927
|
+
const row = [
|
|
928
|
+
category,
|
|
929
|
+
item.id || '',
|
|
930
|
+
`"${(item.title || '').replace(/"/g, '""')}"`,
|
|
931
|
+
`"${(item.content || '').replace(/"/g, '""')}"`,
|
|
932
|
+
`"${(item.source || '').replace(/"/g, '""')}"`,
|
|
933
|
+
item.relevance,
|
|
934
|
+
item.lastUpdated || '',
|
|
935
|
+
`"${(item.tags || []).join(';').replace(/"/g, '""')}"`,
|
|
936
|
+
`"${(item.keywords || []).join(';').replace(/"/g, '""')}"`,
|
|
937
|
+
];
|
|
938
|
+
rows.push(row.join(','));
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return rows.join('\n');
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* 導出為Markdown
|
|
945
|
+
*/
|
|
946
|
+
exportToMarkdown(data) {
|
|
947
|
+
let markdown = '# 知識庫導出\n\n';
|
|
948
|
+
for (const [category, items] of Object.entries(data)) {
|
|
949
|
+
markdown += `## ${category}\n\n`;
|
|
950
|
+
if (items.length === 0) {
|
|
951
|
+
markdown += '*暫無內容*\n\n';
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
items.forEach((item, index) => {
|
|
955
|
+
markdown += `### ${index + 1}. ${item.title}\n\n`;
|
|
956
|
+
markdown += `**來源**: ${item.source}\n\n`;
|
|
957
|
+
markdown += `**相關度**: ${item.relevance}\n\n`;
|
|
958
|
+
markdown += `**最後更新**: ${item.lastUpdated || '未知'}\n\n`;
|
|
959
|
+
if (item.tags && item.tags.length > 0) {
|
|
960
|
+
markdown += `**標簽**: ${item.tags.join(', ')}\n\n`;
|
|
961
|
+
}
|
|
962
|
+
markdown += `**內容**:\n\n${item.content}\n\n`;
|
|
963
|
+
if (item.keywords && item.keywords.length > 0) {
|
|
964
|
+
markdown += `**關鍵詞**: ${item.keywords.join(', ')}\n\n`;
|
|
965
|
+
}
|
|
966
|
+
markdown += '---\n\n';
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return markdown;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* 導出為HTML
|
|
973
|
+
*/
|
|
974
|
+
exportToHTML(data) {
|
|
975
|
+
let html = `<!DOCTYPE html>
|
|
976
|
+
<html lang="zh-TW">
|
|
977
|
+
<head>
|
|
978
|
+
<meta charset="UTF-8">
|
|
979
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
980
|
+
<title>知識庫導出</title>
|
|
981
|
+
<style>
|
|
982
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }
|
|
983
|
+
h1 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
|
|
984
|
+
h2 { color: #555; margin-top: 30px; border-left: 4px solid #4CAF50; padding-left: 10px; }
|
|
985
|
+
h3 { color: #666; margin-top: 20px; }
|
|
986
|
+
.item { background: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin: 15px 0; }
|
|
987
|
+
.meta { color: #777; font-size: 0.9em; margin-bottom: 10px; }
|
|
988
|
+
.tags { margin: 10px 0; }
|
|
989
|
+
.tag { display: inline-block; background: #e0e0e0; padding: 3px 8px; border-radius: 3px; margin-right: 5px; font-size: 0.8em; }
|
|
990
|
+
.content { margin: 15px 0; }
|
|
991
|
+
.keywords { margin-top: 10px; font-style: italic; }
|
|
992
|
+
hr { border: none; border-top: 1px dashed #ccc; margin: 20px 0; }
|
|
993
|
+
</style>
|
|
994
|
+
</head>
|
|
995
|
+
<body>
|
|
996
|
+
<h1>知識庫導出</h1>
|
|
997
|
+
<p>導出時間: ${utils_1.Utils.formatDate()}</p>
|
|
998
|
+
<p>總分類數: ${Object.keys(data).length}</p>
|
|
999
|
+
`;
|
|
1000
|
+
for (const [category, items] of Object.entries(data)) {
|
|
1001
|
+
html += ` <h2>${category}</h2>\n`;
|
|
1002
|
+
html += ` <p>共 ${items.length} 個條目</p>\n`;
|
|
1003
|
+
if (items.length === 0) {
|
|
1004
|
+
html += ' <p><em>暫無內容</em></p>\n';
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
items.forEach((item, index) => {
|
|
1008
|
+
html += ` <div class="item">\n`;
|
|
1009
|
+
html += ` <h3>${index + 1}. ${item.title}</h3>\n`;
|
|
1010
|
+
html += ` <div class="meta">\n`;
|
|
1011
|
+
html += ` <strong>來源</strong>: ${item.source} | \n`;
|
|
1012
|
+
html += ` <strong>相關度</strong>: ${item.relevance} | \n`;
|
|
1013
|
+
html += ` <strong>最後更新</strong>: ${item.lastUpdated || '未知'}\n`;
|
|
1014
|
+
html += ` </div>\n`;
|
|
1015
|
+
if (item.tags && item.tags.length > 0) {
|
|
1016
|
+
html += ` <div class="tags">\n`;
|
|
1017
|
+
html += ` <strong>標簽</strong>: \n`;
|
|
1018
|
+
item.tags.forEach(tag => {
|
|
1019
|
+
html += ` <span class="tag">${tag}</span>\n`;
|
|
1020
|
+
});
|
|
1021
|
+
html += ` </div>\n`;
|
|
1022
|
+
}
|
|
1023
|
+
html += ` <div class="content">\n`;
|
|
1024
|
+
html += ` <strong>內容</strong>:<br>\n`;
|
|
1025
|
+
html += ` ${item.content.replace(/\n/g, '<br>')}\n`;
|
|
1026
|
+
html += ` </div>\n`;
|
|
1027
|
+
if (item.keywords && item.keywords.length > 0) {
|
|
1028
|
+
html += ` <div class="keywords">\n`;
|
|
1029
|
+
html += ` <strong>關鍵詞</strong>: ${item.keywords.join(', ')}\n`;
|
|
1030
|
+
html += ` </div>\n`;
|
|
1031
|
+
}
|
|
1032
|
+
html += ` </div>\n`;
|
|
1033
|
+
});
|
|
1034
|
+
html += ' <hr>\n';
|
|
1035
|
+
}
|
|
1036
|
+
html += `</body>
|
|
1037
|
+
</html>`;
|
|
1038
|
+
return html;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* 記錄日志
|
|
1042
|
+
*/
|
|
1043
|
+
log(message, level = 'info') {
|
|
1044
|
+
if (!this.config.verbose && level === 'debug')
|
|
1045
|
+
return;
|
|
1046
|
+
const timestamp = new Date().toISOString();
|
|
1047
|
+
const prefix = `[${timestamp}] [知識庫]`;
|
|
1048
|
+
switch (level) {
|
|
1049
|
+
case 'debug':
|
|
1050
|
+
if (this.config.logLevel === 'debug') {
|
|
1051
|
+
console.debug(`${prefix} 🔍 ${message}`);
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
case 'info':
|
|
1055
|
+
if (['debug', 'info'].includes(this.config.logLevel)) {
|
|
1056
|
+
console.log(`${prefix} ℹ️ ${message}`);
|
|
1057
|
+
}
|
|
1058
|
+
break;
|
|
1059
|
+
case 'warn':
|
|
1060
|
+
if (['debug', 'info', 'warn'].includes(this.config.logLevel)) {
|
|
1061
|
+
console.warn(`${prefix} ⚠️ ${message}`);
|
|
1062
|
+
}
|
|
1063
|
+
break;
|
|
1064
|
+
case 'error':
|
|
1065
|
+
console.error(`${prefix} ❌ ${message}`);
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* 觸發事件
|
|
1071
|
+
*/
|
|
1072
|
+
emitEvent(event) {
|
|
1073
|
+
this.eventListeners.forEach(listener => {
|
|
1074
|
+
try {
|
|
1075
|
+
listener(event);
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
console.error('事件監聽器執行出錯:', error);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
exports.KnowledgeBase = KnowledgeBase;
|
|
1084
|
+
//# sourceMappingURL=knowledge-base.js.map
|