autosnippet 2.16.0 → 2.18.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/dashboard/dist/assets/index-9byoG7kd.js +129 -0
- package/dashboard/dist/index.html +1 -1
- package/lib/cli/SetupService.js +24 -0
- package/lib/cli/UpgradeService.js +51 -0
- package/lib/external/mcp/handlers/search.js +24 -116
- package/lib/http/routes/search.js +64 -23
- package/lib/service/automation/handlers/SearchHandler.js +95 -6
- package/lib/service/knowledge/KnowledgeService.js +3 -1
- package/lib/service/search/CoarseRanker.js +6 -1
- package/lib/service/search/InvertedIndex.js +4 -16
- package/lib/service/search/MultiSignalRanker.js +6 -1
- package/lib/service/search/SearchEngine.js +213 -25
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +5 -5
- package/dashboard/dist/assets/index-Cdtodtgt.js +0 -123
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>AutoSnippet Dashboard</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-9byoG7kd.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-BkDyUteW.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendor-Ba1BZjav.js">
|
package/lib/cli/SetupService.js
CHANGED
|
@@ -727,6 +727,30 @@ export class SetupService {
|
|
|
727
727
|
console.log(' ✅ .gitignore += .autosnippet-drafts/');
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
+
// ── 必须忽略:_draft_*.md(AI Agent 在项目根目录创建的草稿文件) ──
|
|
731
|
+
if (!content.includes('_draft_*.md')) {
|
|
732
|
+
content += `\n# AutoSnippet AI 草稿文件(项目根目录临时文件)\n_draft_*.md\n`;
|
|
733
|
+
changed = true;
|
|
734
|
+
console.log(' ✅ .gitignore += _draft_*.md');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ── 必须忽略:常见系统 / 编辑器临时文件 ──
|
|
738
|
+
if (!content.includes('.DS_Store')) {
|
|
739
|
+
content += `\n# macOS 元数据\n.DS_Store\n`;
|
|
740
|
+
changed = true;
|
|
741
|
+
console.log(' ✅ .gitignore += .DS_Store');
|
|
742
|
+
}
|
|
743
|
+
if (!content.includes('nohup.out')) {
|
|
744
|
+
content += `nohup.out\n`;
|
|
745
|
+
changed = true;
|
|
746
|
+
console.log(' ✅ .gitignore += nohup.out');
|
|
747
|
+
}
|
|
748
|
+
if (!content.match(/\*\.sw[a-p]/)) {
|
|
749
|
+
content += `*.sw[a-p]\n`;
|
|
750
|
+
changed = true;
|
|
751
|
+
console.log(' ✅ .gitignore += *.sw[a-p]');
|
|
752
|
+
}
|
|
753
|
+
|
|
730
754
|
// Skills 已迁移到 AutoSnippet/skills/(知识库目录内),自动跟随 Git
|
|
731
755
|
|
|
732
756
|
// ── 清理旧版本的 .autosnippet/skills/ negation(已迁移,不再需要)──
|
|
@@ -377,6 +377,57 @@ export class UpgradeService {
|
|
|
377
377
|
console.log(' ✅ 移除旧版 .autosnippet/skills/ 规则(已迁移到 AutoSnippet/skills/)');
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// ── v2.8.1: 新增缺失的 gitignore 规则 ──
|
|
381
|
+
|
|
382
|
+
// _draft_*.md — AI Agent 在项目根目录创建的草稿文件
|
|
383
|
+
if (!content.includes('_draft_*.md')) {
|
|
384
|
+
content += `\n# AutoSnippet AI 草稿文件(项目根目录临时文件)\n_draft_*.md\n`;
|
|
385
|
+
changed = true;
|
|
386
|
+
console.log(' ✅ += _draft_*.md');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// .DS_Store — macOS 元数据
|
|
390
|
+
if (!content.includes('.DS_Store')) {
|
|
391
|
+
content += `\n# macOS 元数据\n.DS_Store\n`;
|
|
392
|
+
changed = true;
|
|
393
|
+
console.log(' ✅ += .DS_Store');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// nohup.out — 后台进程输出
|
|
397
|
+
if (!content.includes('nohup.out')) {
|
|
398
|
+
content += `nohup.out\n`;
|
|
399
|
+
changed = true;
|
|
400
|
+
console.log(' ✅ += nohup.out');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// *.sw[a-p] — vim swap 文件
|
|
404
|
+
if (!content.match(/\*\.sw\[a-p\]/)) {
|
|
405
|
+
content += `*.sw[a-p]\n`;
|
|
406
|
+
changed = true;
|
|
407
|
+
console.log(' ✅ += *.sw[a-p]');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// .autosnippet-drafts/ — AI 草稿目录
|
|
411
|
+
if (!content.includes('.autosnippet-drafts')) {
|
|
412
|
+
content += `\n# AutoSnippet AI 草稿(临时)\n.autosnippet-drafts/\n`;
|
|
413
|
+
changed = true;
|
|
414
|
+
console.log(' ✅ += .autosnippet-drafts/');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// .env — 环境变量
|
|
418
|
+
if (!content.includes('.env') || (!content.match(/^\.env$/m) && !content.match(/^\.env\s/m))) {
|
|
419
|
+
content += `\n# AutoSnippet 环境变量(含 API Key,不入库)\n.env\n`;
|
|
420
|
+
changed = true;
|
|
421
|
+
console.log(' ✅ += .env');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// logs/ — 运行日志
|
|
425
|
+
if (!content.match(/^logs\/?$/m)) {
|
|
426
|
+
content += `\n# AutoSnippet 运行日志\nlogs/\n`;
|
|
427
|
+
changed = true;
|
|
428
|
+
console.log(' ✅ += logs/');
|
|
429
|
+
}
|
|
430
|
+
|
|
380
431
|
// 确保 AutoSnippet/ 不被忽略
|
|
381
432
|
const lines = content.split('\n');
|
|
382
433
|
const hasIgnoreAS = lines.some(l => {
|
|
@@ -68,49 +68,19 @@ export async function search(ctx, args) {
|
|
|
68
68
|
const query = args.query;
|
|
69
69
|
const limit = args.limit || 10;
|
|
70
70
|
const kind = args.kind || args.type || 'all';
|
|
71
|
-
|
|
71
|
+
const mode = args.mode || 'auto';
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
engine.search(query, { mode: 'bm25', limit: limit * 2 }),
|
|
80
|
-
engine.search(query, { mode: 'semantic', limit: limit * 2 }).catch(() => null),
|
|
81
|
-
]);
|
|
82
|
-
const bm25Items = bm25Res?.items || [];
|
|
83
|
-
const semItems = semRes?.items || [];
|
|
84
|
-
|
|
85
|
-
// 融合去重:以 id 为 key,取最高 score
|
|
86
|
-
const merged = new Map();
|
|
87
|
-
for (const it of bm25Items) {
|
|
88
|
-
merged.set(it.id, { ...it, _bm25Score: it.score || 0, _semScore: 0 });
|
|
89
|
-
}
|
|
90
|
-
for (const it of semItems) {
|
|
91
|
-
const existing = merged.get(it.id);
|
|
92
|
-
if (existing) {
|
|
93
|
-
existing._semScore = it.score || 0;
|
|
94
|
-
existing.score = Math.max(existing._bm25Score, existing._semScore);
|
|
95
|
-
} else {
|
|
96
|
-
merged.set(it.id, { ...it, _bm25Score: 0, _semScore: it.score || 0 });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
items = [...merged.values()].sort((a, b) => b.score - a.score);
|
|
100
|
-
actualMode = semItems.length > 0 ? 'auto(bm25+semantic)' : 'auto(bm25-only)';
|
|
101
|
-
} else {
|
|
102
|
-
const result = await engine.search(query, { mode, limit: limit * 2, groupByKind: true });
|
|
103
|
-
items = result?.items || [];
|
|
104
|
-
actualMode = result?.mode || mode;
|
|
105
|
-
}
|
|
73
|
+
// 统一调用 SearchEngine(auto 模式内置 BM25+semantic 融合去重 + Ranking Pipeline)
|
|
74
|
+
const result = await engine.search(query, {
|
|
75
|
+
mode, limit: kind !== 'all' ? limit * 2 : limit, rank: true, groupByKind: true,
|
|
76
|
+
});
|
|
77
|
+
let items = result?.items || [];
|
|
78
|
+
const actualMode = result?.mode || mode;
|
|
106
79
|
|
|
107
80
|
// kind 过滤
|
|
108
81
|
items = filterByKind(items, kind);
|
|
109
82
|
items = items.slice(0, limit);
|
|
110
83
|
|
|
111
|
-
// 清理内部字段
|
|
112
|
-
for (const it of items) { delete it._bm25Score; delete it._semScore; }
|
|
113
|
-
|
|
114
84
|
const byKind = groupByKind(items);
|
|
115
85
|
const elapsed = Date.now() - t0;
|
|
116
86
|
|
|
@@ -132,57 +102,31 @@ export async function search(ctx, args) {
|
|
|
132
102
|
// ─── 2. autosnippet_context_search — 智能上下文搜索 ────────────
|
|
133
103
|
|
|
134
104
|
/**
|
|
135
|
-
* 智能上下文搜索 ——
|
|
105
|
+
* 智能上下文搜索 —— SearchEngine 内置 Ranking Pipeline
|
|
136
106
|
*
|
|
137
107
|
* 设计原则:MCP 调用方是外部 AI Agent,意图识别由 Agent 自行完成。
|
|
138
|
-
* 本工具聚焦数据检索:
|
|
139
|
-
* 不使用 AgentCoordinator 做意图分析。
|
|
108
|
+
* 本工具聚焦数据检索:BM25 召回 + CoarseRanker + MultiSignalRanker + 上下文加成
|
|
140
109
|
*
|
|
141
110
|
* 特色:byKind 分组、个性化推荐、会话连续性
|
|
142
111
|
*/
|
|
143
112
|
export async function contextSearch(ctx, args) {
|
|
144
113
|
const t0 = Date.now();
|
|
145
|
-
const query = args.query;
|
|
146
|
-
const limit = args.limit ?? 5;
|
|
147
|
-
let source = 'search-engine';
|
|
148
|
-
let items = [];
|
|
149
|
-
|
|
150
|
-
// 引擎只获取一次,两级降级共用
|
|
151
114
|
const engine = getSearchEngine(ctx) || await getFallbackEngine(ctx);
|
|
115
|
+
const limit = args.limit ?? 5;
|
|
152
116
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
intent: 'search',
|
|
162
|
-
language: args.language,
|
|
163
|
-
sessionHistory: args.sessionHistory || [],
|
|
164
|
-
});
|
|
165
|
-
source = 'retrieval-funnel';
|
|
166
|
-
} else {
|
|
167
|
-
items = candidates;
|
|
168
|
-
source = 'search-engine';
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
// Funnel 失败,继续降级
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ── 降级:直接 SearchEngine ──
|
|
175
|
-
if (items.length === 0) {
|
|
176
|
-
try {
|
|
177
|
-
const result = await engine.search(query, { mode: 'bm25', limit, groupByKind: true });
|
|
178
|
-
items = result?.items || [];
|
|
179
|
-
source = 'search-engine';
|
|
180
|
-
} catch { /* 如果连基础搜索也失败 */ }
|
|
181
|
-
}
|
|
117
|
+
const result = await engine.search(args.query, {
|
|
118
|
+
mode: 'bm25', limit, rank: true, groupByKind: true,
|
|
119
|
+
context: {
|
|
120
|
+
intent: 'search',
|
|
121
|
+
language: args.language,
|
|
122
|
+
sessionHistory: args.sessionHistory || [],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
182
125
|
|
|
183
|
-
items = items.slice(0, limit);
|
|
126
|
+
const items = (result?.items || []).slice(0, limit);
|
|
184
127
|
const byKind = groupByKind(items);
|
|
185
128
|
const elapsed = Date.now() - t0;
|
|
129
|
+
const source = result?.ranked ? 'search-engine+ranking' : 'search-engine';
|
|
186
130
|
|
|
187
131
|
return envelope({
|
|
188
132
|
success: true,
|
|
@@ -199,35 +143,6 @@ export async function contextSearch(ctx, args) {
|
|
|
199
143
|
});
|
|
200
144
|
}
|
|
201
145
|
|
|
202
|
-
/**
|
|
203
|
-
* 将 SearchEngine 输出规范化为 RetrievalFunnel 期望的结构
|
|
204
|
-
* SearchEngine 返回: { id, title, trigger, description, content, kind, status, score }
|
|
205
|
-
* Funnel 期望: { id, title, content, code, description, score, language, category, tags, ... }
|
|
206
|
-
*/
|
|
207
|
-
function normalizeFunnelInput(items) {
|
|
208
|
-
return items.map(item => {
|
|
209
|
-
// 解析 content JSON → 可搜索文本
|
|
210
|
-
let contentText = '';
|
|
211
|
-
let codeText = '';
|
|
212
|
-
try {
|
|
213
|
-
const parsed = JSON.parse(item.content || '{}');
|
|
214
|
-
contentText = [parsed.rationale, parsed.markdown].filter(Boolean).join(' ');
|
|
215
|
-
codeText = parsed.pattern || '';
|
|
216
|
-
} catch { /* ignore */ }
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
...item,
|
|
220
|
-
content: contentText,
|
|
221
|
-
code: codeText,
|
|
222
|
-
// 将 SearchEngine 的 score 映射为 Funnel 需要的 bm25Score
|
|
223
|
-
bm25Score: item.score || 0,
|
|
224
|
-
// 信号补充(从 SearchEngine 结果推导,不完美但比 0 好)
|
|
225
|
-
qualityScore: item.status === 'active' ? 70 : 40,
|
|
226
|
-
usageCount: 0, // SearchEngine 无此信息,留给 Funnel 默认
|
|
227
|
-
};
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
146
|
// ─── 3. autosnippet_keyword_search — SQL LIKE 精确匹配 ─────────
|
|
232
147
|
|
|
233
148
|
/**
|
|
@@ -245,15 +160,11 @@ export async function keywordSearch(ctx, args) {
|
|
|
245
160
|
const kind = args.kind || 'all';
|
|
246
161
|
|
|
247
162
|
const result = await engine.search(query, {
|
|
248
|
-
mode: 'keyword',
|
|
249
|
-
type: kind === 'rule' ? 'rule' : 'all',
|
|
250
|
-
limit,
|
|
251
|
-
groupByKind: true,
|
|
163
|
+
mode: 'keyword', limit, groupByKind: true,
|
|
252
164
|
});
|
|
253
165
|
|
|
254
166
|
let items = result?.items || [];
|
|
255
|
-
items = filterByKind(items, kind);
|
|
256
|
-
items = items.slice(0, limit);
|
|
167
|
+
items = filterByKind(items, kind).slice(0, limit);
|
|
257
168
|
const byKind = groupByKind(items);
|
|
258
169
|
const elapsed = Date.now() - t0;
|
|
259
170
|
|
|
@@ -289,15 +200,12 @@ export async function semanticSearch(ctx, args) {
|
|
|
289
200
|
const kind = args.kind || 'all';
|
|
290
201
|
|
|
291
202
|
const result = await engine.search(query, {
|
|
292
|
-
mode: 'semantic',
|
|
293
|
-
limit: limit * 2,
|
|
294
|
-
groupByKind: true,
|
|
203
|
+
mode: 'semantic', limit: limit * 2, rank: true, groupByKind: true,
|
|
295
204
|
});
|
|
296
205
|
|
|
297
206
|
let items = result?.items || [];
|
|
298
207
|
const actualMode = result?.mode || 'semantic';
|
|
299
|
-
items = filterByKind(items, kind);
|
|
300
|
-
items = items.slice(0, limit);
|
|
208
|
+
items = filterByKind(items, kind).slice(0, limit);
|
|
301
209
|
const byKind = groupByKind(items);
|
|
302
210
|
const elapsed = Date.now() - t0;
|
|
303
211
|
|
|
@@ -30,15 +30,13 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
30
30
|
|
|
31
31
|
const container = getServiceContainer();
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
logger.warn('SearchEngine 搜索失败,降级到传统搜索', { mode, error: err.message });
|
|
41
|
-
}
|
|
33
|
+
// 所有模式优先通过 SearchEngine(含 auto/bm25/semantic/keyword/ranking)
|
|
34
|
+
try {
|
|
35
|
+
const searchEngine = container.get('searchEngine');
|
|
36
|
+
const result = await searchEngine.search(q, { type, limit, mode, groupByKind });
|
|
37
|
+
return res.json({ success: true, data: result });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logger.warn('SearchEngine 搜索失败,降级到传统搜索', { mode, error: err.message });
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
const results = {};
|
|
@@ -213,29 +211,72 @@ router.get('/graph/stats', asyncHandler(async (req, res) => {
|
|
|
213
211
|
|
|
214
212
|
/**
|
|
215
213
|
* POST /api/v1/search/context-aware
|
|
216
|
-
* 上下文感知搜索
|
|
214
|
+
* 上下文感知搜索 — SearchEngine 内置 Ranking Pipeline(CoarseRanker + MultiSignalRanker + ContextBoost)
|
|
217
215
|
*/
|
|
218
216
|
router.post('/context-aware', asyncHandler(async (req, res) => {
|
|
219
|
-
const { keyword, limit } = req.body;
|
|
217
|
+
const { keyword, limit, language, sessionHistory } = req.body;
|
|
220
218
|
if (!keyword || !keyword.trim()) {
|
|
221
219
|
throw new ValidationError('keyword is required');
|
|
222
220
|
}
|
|
221
|
+
const t0 = Date.now();
|
|
223
222
|
const container = getServiceContainer();
|
|
224
|
-
const knowledgeService = container.get('knowledgeService');
|
|
225
223
|
const pageSize = Math.min(limit || 10, 100);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
224
|
+
let results = [];
|
|
225
|
+
let source = 'knowledgeService';
|
|
226
|
+
|
|
227
|
+
// SearchEngine BM25 + 内置 Ranking Pipeline
|
|
228
|
+
try {
|
|
229
|
+
const searchEngine = container.get('searchEngine');
|
|
230
|
+
const result = await searchEngine.search(keyword, {
|
|
231
|
+
mode: 'bm25', limit: pageSize, rank: true,
|
|
232
|
+
context: { intent: 'search', language, sessionHistory: sessionHistory || [] },
|
|
233
|
+
});
|
|
234
|
+
const items = result?.items || [];
|
|
235
|
+
if (items.length > 0) {
|
|
236
|
+
source = result.ranked ? 'search-engine+ranking' : 'search-engine';
|
|
237
|
+
results = items.map(r => {
|
|
238
|
+
let contentStr = '';
|
|
239
|
+
try {
|
|
240
|
+
const c = typeof r.content === 'string' && r.content.startsWith('{') ? JSON.parse(r.content) : (r.content || {});
|
|
241
|
+
contentStr = c.pattern || c.markdown || c.code || '';
|
|
242
|
+
} catch { contentStr = r.content || r.code || ''; }
|
|
243
|
+
return {
|
|
244
|
+
name: (r.title || r.id) + '.md',
|
|
245
|
+
content: contentStr,
|
|
246
|
+
similarity: r.score || 0,
|
|
247
|
+
authority: r.authorityScore || 0,
|
|
248
|
+
matchType: result.ranked ? 'ranked' : 'bm25',
|
|
249
|
+
qualityScore: r.qualityScore || 0,
|
|
250
|
+
usageCount: r.usageCount || 0,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
logger.warn('SearchEngine context-aware 失败,降级到 KnowledgeService', { error: err.message });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 降级: KnowledgeService SQL LIKE
|
|
259
|
+
if (results.length === 0) {
|
|
260
|
+
try {
|
|
261
|
+
const knowledgeService = container.get('knowledgeService');
|
|
262
|
+
const list = await knowledgeService.search(keyword, { page: 1, pageSize });
|
|
263
|
+
const items = list.data || list.items || [];
|
|
264
|
+
results = items.map(r => ({
|
|
265
|
+
name: (r.title || r.id) + '.md',
|
|
266
|
+
content: (r.content || {}).pattern || (r.content || {}).markdown || '',
|
|
267
|
+
similarity: 1,
|
|
268
|
+
authority: (r.quality || {}).overall || 0,
|
|
269
|
+
matchType: 'keyword',
|
|
270
|
+
qualityScore: (r.quality || {}).overall || 0,
|
|
271
|
+
}));
|
|
272
|
+
source = 'knowledgeService';
|
|
273
|
+
} catch { /* 全部失败 */ }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const elapsed = Date.now() - t0;
|
|
236
277
|
res.json({
|
|
237
278
|
success: true,
|
|
238
|
-
data: { results, context: {}, total:
|
|
279
|
+
data: { results, context: {}, total: results.length, hasAiEvaluation: false, searchTime: elapsed, source },
|
|
239
280
|
});
|
|
240
281
|
}));
|
|
241
282
|
|
|
@@ -26,7 +26,8 @@ export async function handleSearch(watcher, fullPath, relativePath, searchLine)
|
|
|
26
26
|
const container = ServiceContainer.getInstance();
|
|
27
27
|
const searchEngine = container.get('searchEngine');
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// 诊断:确保索引已构建后再输出状态
|
|
30
|
+
searchEngine.ensureIndex();
|
|
30
31
|
const stats = searchEngine.getStats();
|
|
31
32
|
if (stats.totalDocuments === 0) {
|
|
32
33
|
console.log(` ⚠️ 知识库为空(索引 0 条记录),请先通过 asd setup / Dashboard 添加知识条目`);
|
|
@@ -34,14 +35,21 @@ export async function handleSearch(watcher, fullPath, relativePath, searchLine)
|
|
|
34
35
|
console.log(` 📊 索引 ${stats.totalDocuments} 条知识`);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// BM25 → keyword
|
|
38
|
+
// auto (BM25+semantic 融合 + Ranking Pipeline) → keyword (SQL LIKE) 降级链
|
|
39
|
+
// Xcode/IDE 场景: 传递 generate intent,让排序器使用代码生成权重
|
|
38
40
|
try {
|
|
39
|
-
results = await searchEngine.search(query, {
|
|
41
|
+
results = await searchEngine.search(query, {
|
|
42
|
+
limit: 10, mode: 'auto', rank: true,
|
|
43
|
+
context: { intent: 'generate' },
|
|
44
|
+
});
|
|
45
|
+
// auto 零结果 → keyword (SQL LIKE) 兆底
|
|
40
46
|
if (!results || (results.items || []).length === 0) {
|
|
41
47
|
results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
|
|
42
48
|
}
|
|
43
49
|
} catch {
|
|
44
|
-
|
|
50
|
+
try {
|
|
51
|
+
results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
|
|
52
|
+
} catch { /* 全部失败 */ }
|
|
45
53
|
}
|
|
46
54
|
} catch (err) {
|
|
47
55
|
console.warn(` ⚠️ 搜索失败: ${err.message}`);
|
|
@@ -52,6 +60,13 @@ export async function handleSearch(watcher, fullPath, relativePath, searchLine)
|
|
|
52
60
|
|
|
53
61
|
const items = normalizeSearchResults(results);
|
|
54
62
|
|
|
63
|
+
// Xcode 代码插入场景: 有实际代码的结果优先展示
|
|
64
|
+
items.sort((a, b) => {
|
|
65
|
+
const aHasCode = a.code && a.code !== '(无预览内容)' && a.code.length > 30 ? 1 : 0;
|
|
66
|
+
const bHasCode = b.code && b.code !== '(无预览内容)' && b.code.length > 30 ? 1 : 0;
|
|
67
|
+
return bHasCode - aHasCode;
|
|
68
|
+
});
|
|
69
|
+
|
|
55
70
|
if (items.length === 0) {
|
|
56
71
|
console.log(` ℹ️ 未找到「${query}」的相关结果`);
|
|
57
72
|
watcher._notify(`未找到「${query}」的相关结果`);
|
|
@@ -114,7 +129,21 @@ export function normalizeSearchResults(results) {
|
|
|
114
129
|
if (Array.isArray(content.headers) && content.headers.length > 0) {
|
|
115
130
|
headers = content.headers;
|
|
116
131
|
}
|
|
117
|
-
|
|
132
|
+
// 如果主字段为空,尝试从 Markdown 内容提取代码块
|
|
133
|
+
if (!code && content.markdown) {
|
|
134
|
+
const fenced = content.markdown.match(/```[\w]*\n([\s\S]*?)```/);
|
|
135
|
+
if (fenced) code = fenced[1].trim();
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// content 不是 JSON,可能是纯文本/代码 — 直接使用
|
|
139
|
+
if (typeof r.content === 'string' && r.content.length > 10) {
|
|
140
|
+
code = r.content.substring(0, 2000);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 如果 Ranking Pipeline 已提取 code 字段,优先使用
|
|
145
|
+
if (!code && r.code && r.code.length > 5) {
|
|
146
|
+
code = r.code;
|
|
118
147
|
}
|
|
119
148
|
// V3: headers 是独立 JSON 列(字符串),优先解析
|
|
120
149
|
if (headers.length === 0 && r.headers) {
|
|
@@ -137,9 +166,20 @@ export function normalizeSearchResults(results) {
|
|
|
137
166
|
moduleName = r.moduleName || null;
|
|
138
167
|
}
|
|
139
168
|
|
|
169
|
+
// ── 从 code 中分离 #import / @import / import 行,归入 headers ──
|
|
170
|
+
const finalCode = code || r.code || r.description || r.trigger || '(无预览内容)';
|
|
171
|
+
const { cleanedCode, extractedHeaders } = _separateImportsFromCode(finalCode);
|
|
172
|
+
if (extractedHeaders.length > 0) {
|
|
173
|
+
for (const h of extractedHeaders) {
|
|
174
|
+
if (!headers.some(existing => existing.trim() === h.trim())) {
|
|
175
|
+
headers.push(h);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
140
180
|
return {
|
|
141
181
|
title: r.title || r.name || r.id || 'Recipe',
|
|
142
|
-
code:
|
|
182
|
+
code: cleanedCode || '(无预览内容)',
|
|
143
183
|
explanation: explanation || r.summary || r.description || '',
|
|
144
184
|
headers,
|
|
145
185
|
moduleName,
|
|
@@ -147,3 +187,52 @@ export function normalizeSearchResults(results) {
|
|
|
147
187
|
};
|
|
148
188
|
}).filter(item => item.title);
|
|
149
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 从代码文本中分离出 import/include 行
|
|
193
|
+
*
|
|
194
|
+
* 只提取位于代码开头的连续 import 块(含中间空行),
|
|
195
|
+
* 代码正文中的 import(如注释或字符串里的)不做处理。
|
|
196
|
+
*
|
|
197
|
+
* 支持: #import, @import, #include, import (Swift)
|
|
198
|
+
*/
|
|
199
|
+
function _separateImportsFromCode(code) {
|
|
200
|
+
if (!code || code === '(无预览内容)') {
|
|
201
|
+
return { cleanedCode: code, extractedHeaders: [] };
|
|
202
|
+
}
|
|
203
|
+
const lines = code.split(/\r?\n/);
|
|
204
|
+
const importRe = /^\s*(#import\s|@import\s|#include\s|import\s)/;
|
|
205
|
+
const extractedHeaders = [];
|
|
206
|
+
let lastImportIdx = -1;
|
|
207
|
+
|
|
208
|
+
// 从开头扫描连续 import 块(允许中间有空行)
|
|
209
|
+
for (let i = 0; i < lines.length; i++) {
|
|
210
|
+
const trimmed = lines[i].trim();
|
|
211
|
+
if (!trimmed) {
|
|
212
|
+
// 空行:如果前面已有 import,继续扫描
|
|
213
|
+
if (lastImportIdx >= 0) continue;
|
|
214
|
+
// 前面没 import,遇到前导空行也继续
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (importRe.test(trimmed)) {
|
|
218
|
+
extractedHeaders.push(trimmed);
|
|
219
|
+
lastImportIdx = i;
|
|
220
|
+
} else {
|
|
221
|
+
// 遇到非 import 非空行,停止扫描
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (extractedHeaders.length === 0) {
|
|
227
|
+
return { cleanedCode: code, extractedHeaders: [] };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 移除开头的 import 行和紧随的空行
|
|
231
|
+
const remaining = lines.slice(lastImportIdx + 1);
|
|
232
|
+
// 去掉残留的前导空行
|
|
233
|
+
while (remaining.length > 0 && !remaining[0].trim()) {
|
|
234
|
+
remaining.shift();
|
|
235
|
+
}
|
|
236
|
+
const cleanedCode = remaining.join('\n').trim();
|
|
237
|
+
return { cleanedCode, extractedHeaders };
|
|
238
|
+
}
|
|
@@ -529,9 +529,11 @@ export class KnowledgeService {
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
// 构建 DB 更新
|
|
532
|
+
// 注意: 不在此处 JSON.stringify — repository.update() 内部
|
|
533
|
+
// 通过 _entityToRow() 统一执行序列化, 传入原始值即可
|
|
532
534
|
const dbUpdates = {
|
|
533
535
|
lifecycle: entry.lifecycle,
|
|
534
|
-
lifecycleHistory:
|
|
536
|
+
lifecycleHistory: entry.lifecycleHistory,
|
|
535
537
|
updatedAt: entry.updatedAt,
|
|
536
538
|
};
|
|
537
539
|
|
|
@@ -76,7 +76,12 @@ export class CoarseRanker {
|
|
|
76
76
|
#computeFreshness(candidate) {
|
|
77
77
|
const updated = candidate.updatedAt || candidate.lastModified || candidate.createdAt;
|
|
78
78
|
if (!updated) return 0.5;
|
|
79
|
-
|
|
79
|
+
// 自动识别秒级/毫秒级 Unix 时间戳 (秒级 ≤ 9999999999 即 2286 年)
|
|
80
|
+
const ts = typeof updated === 'number' && updated > 0 && updated <= 9999999999
|
|
81
|
+
? updated * 1000
|
|
82
|
+
: (typeof updated === 'number' ? updated : new Date(updated).getTime());
|
|
83
|
+
const ageDays = (Date.now() - ts) / 86400000;
|
|
84
|
+
if (ageDays < 0) return 1.0; // 未来时间戳视为最新
|
|
80
85
|
return Math.exp(-0.693 * ageDays / 180); // 半衰期 180 天
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -3,22 +3,10 @@
|
|
|
3
3
|
* 构建和查询 token → docIndex 映射
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @returns {string[]}
|
|
11
|
-
*/
|
|
12
|
-
export function tokenize(text) {
|
|
13
|
-
if (!text || typeof text !== 'string') return [];
|
|
14
|
-
// 拆分 camelCase/PascalCase(与 SearchEngine.tokenize 一致)
|
|
15
|
-
const expanded = text.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
16
|
-
const tokens = expanded
|
|
17
|
-
.toLowerCase()
|
|
18
|
-
.match(/[\p{L}\p{N}_]+/gu) || [];
|
|
19
|
-
// 过滤过短 token(≥2 字符),减少噪声
|
|
20
|
-
return tokens.filter(t => t.length >= 2);
|
|
21
|
-
}
|
|
6
|
+
// 使用 SearchEngine 的统一分词器(含完整 CJK 单字/bigram 支持)
|
|
7
|
+
// 确保倒排索引与 BM25 搜索使用一致的分词策略,避免中文查询召回率差异
|
|
8
|
+
import { tokenize } from './SearchEngine.js';
|
|
9
|
+
export { tokenize };
|
|
22
10
|
|
|
23
11
|
/**
|
|
24
12
|
* 构建倒排索引
|
|
@@ -61,7 +61,12 @@ export class RecencySignal {
|
|
|
61
61
|
compute(candidate) {
|
|
62
62
|
const updated = candidate.updatedAt || candidate.lastModified || candidate.createdAt;
|
|
63
63
|
if (!updated) return 0.5;
|
|
64
|
-
|
|
64
|
+
// 自动识别秒级/毫秒级 Unix 时间戳 (秒级 ≤ 9999999999 即 2286 年)
|
|
65
|
+
const ts = typeof updated === 'number' && updated > 0 && updated <= 9999999999
|
|
66
|
+
? updated * 1000
|
|
67
|
+
: (typeof updated === 'number' ? updated : new Date(updated).getTime());
|
|
68
|
+
const ageMs = Date.now() - ts;
|
|
69
|
+
if (ageMs < 0) return 1.0; // 未来时间戳视为最新
|
|
65
70
|
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
66
71
|
// 指数衰减:半衰期 90 天
|
|
67
72
|
return Math.exp(-0.693 * ageDays / 90);
|