autosnippet 2.16.1 → 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.
@@ -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-Cl1XJniU.js"></script>
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">
@@ -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
- let mode = args.mode || 'auto';
71
+ const mode = args.mode || 'auto';
72
72
 
73
- let items;
74
- let actualMode = mode;
75
-
76
- if (mode === 'auto') {
77
- // 同时做 BM25 + semantic,融合取最优
78
- const [bm25Res, semRes] = await Promise.all([
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
- * 智能上下文搜索 —— RetrievalFunnel 4 层漏斗 + SearchEngine 降级
105
+ * 智能上下文搜索 —— SearchEngine 内置 Ranking Pipeline
136
106
  *
137
107
  * 设计原则:MCP 调用方是外部 AI Agent,意图识别由 Agent 自行完成。
138
- * 本工具聚焦数据检索:RetrievalFunnel 多层精炼 SearchEngine BM25 降级。
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
- // ── 优先:RetrievalFunnel 4 层漏斗 ──
154
- try {
155
- const funnel = ctx.container.get('retrievalFunnel');
156
- const rawResult = await engine.search(query, { mode: 'bm25', limit: limit * 3, groupByKind: true });
157
- const candidates = rawResult?.items || [];
158
- if (funnel && candidates.length > 0) {
159
- const normalized = normalizeFunnelInput(candidates);
160
- items = await funnel.execute(query, normalized, {
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', // SQL LIKE —— 区别于 BM25
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
- // 如果指定了 mode (bm25/semantic),使用 SearchEngine 直接搜索
34
- if (mode === 'bm25' || mode === 'semantic' || mode === 'ranking') {
35
- try {
36
- const searchEngine = container.get('searchEngine');
37
- const result = await searchEngine.search(q, { type, limit, mode, groupByKind });
38
- return res.json({ success: true, data: result });
39
- } catch (err) {
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
- const list = await knowledgeService.search(keyword, { page: 1, pageSize });
227
- const items = list.data || list.items || [];
228
- const results = items.map(r => ({
229
- name: (r.title || r.id) + '.md',
230
- content: (r.content || {}).pattern || (r.content || {}).markdown || '',
231
- similarity: 1,
232
- authority: (r.quality || {}).overall || 0,
233
- matchType: 'keyword',
234
- qualityScore: (r.quality || {}).overall || 0,
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: list.total || results.length, hasAiEvaluation: false, searchTime: 0 },
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 逐级降级:空结果也触发降级(中文分词不足时 BM25 可能零命中)
38
+ // auto (BM25+semantic 融合 + Ranking Pipeline) → keyword (SQL LIKE) 降级链
39
+ // Xcode/IDE 场景: 传递 generate intent,让排序器使用代码生成权重
38
40
  try {
39
- results = await searchEngine.search(query, { limit: 10, mode: 'bm25' });
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
- results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
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
- } catch { /* ignore */ }
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: code || r.code || r.description || r.trigger || '(无预览内容)',
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: JSON.stringify(entry.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
- const ageDays = (Date.now() - new Date(updated).getTime()) / 86400000;
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
- * Unicode-aware 分词(含 camelCase 拆分 + 最小长度过滤)
8
- * SearchEngine.tokenize 保持一致的拆分策略
9
- * @param {string} text
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
- const ageMs = Date.now() - new Date(updated).getTime();
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);