autosnippet 3.4.0 → 3.4.2
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 +43 -18
- package/dashboard/dist/assets/{index-8b1Gf3Bb.js → index-BX6r2fiy.js} +40 -40
- package/dashboard/dist/assets/index-BvZcGN02.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/lib/core/AstAnalyzer.js +0 -1
- package/dist/lib/core/ast/lang-dart.js +118 -8
- package/dist/lib/core/ast/lang-go.js +0 -1
- package/dist/lib/core/ast/lang-java.js +25 -11
- package/dist/lib/core/ast/lang-javascript.js +103 -17
- package/dist/lib/core/ast/lang-objc.d.ts +1 -1
- package/dist/lib/core/ast/lang-objc.js +80 -4
- package/dist/lib/core/ast/lang-python.js +0 -1
- package/dist/lib/core/ast/lang-rust.js +0 -1
- package/dist/lib/core/ast/lang-swift.d.ts +1 -1
- package/dist/lib/core/ast/lang-swift.js +184 -7
- package/dist/lib/core/ast/lang-typescript.js +0 -1
- package/dist/lib/external/ai/AiFactory.d.ts +14 -0
- package/dist/lib/external/ai/AiFactory.js +33 -1
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +7 -3
- package/dist/lib/external/ai/providers/OpenAiProvider.js +1 -1
- package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.d.ts +33 -1
- package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +392 -19
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -1
- package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +2 -0
- package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +4 -0
- package/dist/lib/external/mcp/handlers/guard.js +11 -6
- package/dist/lib/http/routes/ai.js +18 -1
- package/dist/lib/infrastructure/vector/IndexingPipeline.js +6 -1
- package/dist/lib/injection/modules/AiModule.js +22 -1
- package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +7 -0
- package/dist/lib/service/bootstrap/BootstrapTaskManager.js +17 -0
- package/dist/lib/service/guard/ComplianceReporter.js +5 -1
- package/dist/lib/service/guard/GuardCheckEngine.d.ts +12 -1
- package/dist/lib/service/guard/GuardCheckEngine.js +36 -4
- package/dist/lib/service/guard/GuardCodeChecks.js +27 -9
- package/dist/lib/service/guard/SourceFileCollector.d.ts +3 -2
- package/dist/lib/service/guard/SourceFileCollector.js +3 -3
- package/dist/lib/service/search/SearchEngine.js +165 -61
- package/dist/lib/service/task/PrimeSearchPipeline.js +17 -2
- package/dist/lib/service/vector/VectorService.js +10 -1
- package/dist/lib/shared/LanguageService.d.ts +12 -0
- package/dist/lib/shared/LanguageService.js +85 -0
- package/dist/lib/shared/schemas/http-requests.d.ts +4 -0
- package/dist/lib/shared/schemas/http-requests.js +4 -0
- package/dist/lib/types/project-snapshot.d.ts +1 -0
- package/package.json +1 -1
- package/resources/grammars/tree-sitter-dart.wasm +0 -0
- package/dashboard/dist/assets/index-DHJ1Dj7u.css +0 -1
|
@@ -110,6 +110,7 @@ export class SearchEngine {
|
|
|
110
110
|
async search(query, options = {}) {
|
|
111
111
|
const { type = 'all', limit = 20, mode = 'keyword', context } = options;
|
|
112
112
|
const shouldRank = options.rank ?? mode !== 'keyword';
|
|
113
|
+
const tSearchStart = performance.now();
|
|
113
114
|
if (!query || !query.trim()) {
|
|
114
115
|
return { items: [], total: 0, query };
|
|
115
116
|
}
|
|
@@ -132,60 +133,62 @@ export class SearchEngine {
|
|
|
132
133
|
let actualMode = mode;
|
|
133
134
|
switch (mode) {
|
|
134
135
|
case 'auto': {
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
136
|
+
// ── Weighted-First + Confidence Gate ──
|
|
137
|
+
// 先跑 weighted(~40ms),评估是否需要 embed(2-22s)
|
|
138
|
+
const weightedItems = this._scorerSearch(query, type, recallLimit);
|
|
139
|
+
const confidence = this.#computeWeightedConfidence(query, weightedItems, limit);
|
|
140
|
+
if (confidence >= 60 || !this.vectorService) {
|
|
141
|
+
// 高 confidence: weighted 已足够,跳过 embed
|
|
142
|
+
results = weightedItems;
|
|
143
|
+
actualMode = `auto(weighted-only,conf=${confidence})`;
|
|
144
|
+
this.logger.info(`[QueryRouter] skip-semantic: conf=${confidence} topScore=${weightedItems[0]?.score ?? 0} query="${query}"`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
// 低 confidence: 投入 embed,RRF 融合
|
|
148
|
+
// 自适应 alpha:confidence 越低 → semantic 权重越高
|
|
149
|
+
// conf=0 → alpha=0.75, conf=30 → alpha=0.575, conf=55 → alpha=0.42
|
|
150
|
+
const adaptiveAlpha = this._fusionSemanticWeight + (0.75 - this._fusionSemanticWeight) * (1 - confidence / 60);
|
|
151
|
+
this.logger.info(`[QueryRouter] invoke-semantic: conf=${confidence} alpha=${adaptiveAlpha.toFixed(2)} topScore=${weightedItems[0]?.score ?? 0} query="${query}"`);
|
|
152
|
+
try {
|
|
153
|
+
const rrfResults = await this.vectorService.hybridSearch(query, {
|
|
154
|
+
topK: recallLimit,
|
|
155
|
+
alpha: adaptiveAlpha,
|
|
156
|
+
sparseSearchFn: () => weightedItems,
|
|
157
|
+
});
|
|
158
|
+
if (rrfResults.length > 0) {
|
|
159
|
+
results = rrfResults.map((r) => {
|
|
160
|
+
const base = r.data?.item ||
|
|
161
|
+
r.data ||
|
|
162
|
+
{};
|
|
163
|
+
const baseMeta = (base.metadata || {});
|
|
164
|
+
return {
|
|
165
|
+
id: r.id,
|
|
166
|
+
title: (base.title ||
|
|
167
|
+
baseMeta.title ||
|
|
168
|
+
r.id),
|
|
169
|
+
type: (base.type || 'recipe'),
|
|
170
|
+
kind: (base.kind ||
|
|
171
|
+
baseMeta.kind ||
|
|
172
|
+
'pattern'),
|
|
173
|
+
status: (base.status ||
|
|
174
|
+
baseMeta.status ||
|
|
175
|
+
'active'),
|
|
176
|
+
score: Math.round(r.score * 1000) / 1000,
|
|
177
|
+
content: base.content,
|
|
178
|
+
description: base.description,
|
|
179
|
+
};
|
|
151
180
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
r.data ||
|
|
156
|
-
{};
|
|
157
|
-
const baseMeta = (base.metadata || {});
|
|
158
|
-
return {
|
|
159
|
-
id: r.id,
|
|
160
|
-
title: (base.title ||
|
|
161
|
-
baseMeta.title ||
|
|
162
|
-
r.id),
|
|
163
|
-
type: (base.type || 'recipe'),
|
|
164
|
-
kind: (base.kind ||
|
|
165
|
-
baseMeta.kind ||
|
|
166
|
-
'pattern'),
|
|
167
|
-
status: (base.status ||
|
|
168
|
-
baseMeta.status ||
|
|
169
|
-
'active'),
|
|
170
|
-
score: Math.round(r.score * 1000) / 1000,
|
|
171
|
-
content: base.content,
|
|
172
|
-
description: base.description,
|
|
173
|
-
};
|
|
174
|
-
});
|
|
175
|
-
this._supplementDetails(results);
|
|
176
|
-
actualMode = 'auto(rrf)';
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// VectorService RRF 失败, 降级到 min-max 融合
|
|
181
|
+
this._supplementDetails(results);
|
|
182
|
+
actualMode = `auto(rrf,conf=${confidence},α=${adaptiveAlpha.toFixed(2)})`;
|
|
183
|
+
break;
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
catch {
|
|
187
|
+
// VectorService RRF 失败, 降级
|
|
188
|
+
}
|
|
189
|
+
// 降级: embed 失败 → 返回已有的 weighted 结果
|
|
190
|
+
results = weightedItems;
|
|
191
|
+
actualMode = `auto(weighted-fallback,conf=${confidence})`;
|
|
189
192
|
break;
|
|
190
193
|
}
|
|
191
194
|
case 'weighted':
|
|
@@ -215,6 +218,9 @@ export class SearchEngine {
|
|
|
215
218
|
type,
|
|
216
219
|
ranked: shouldRank && results.length > 0,
|
|
217
220
|
};
|
|
221
|
+
// ── 搜索计时日志 ──
|
|
222
|
+
const tSearchEnd = performance.now();
|
|
223
|
+
this.logger.info(`Search completed: mode=${actualMode} total=${results.length} time=${Math.round(tSearchEnd - tSearchStart)}ms ranked=${response.ranked} query="${query}"`);
|
|
218
224
|
if (options.groupByKind) {
|
|
219
225
|
response.byKind = { rule: [], pattern: [], fact: [] };
|
|
220
226
|
for (const r of results) {
|
|
@@ -448,15 +454,20 @@ export class SearchEngine {
|
|
|
448
454
|
let results = vectorResults.map((vr) => {
|
|
449
455
|
const item = vr.item;
|
|
450
456
|
const metadata = (item.metadata || {});
|
|
457
|
+
const rawId = item.id || '';
|
|
458
|
+
// 从 vector ID 提取 DB entryId: "entry_<uuid>" → "<uuid>"
|
|
459
|
+
const entryId = metadata.entryId || rawId.replace(/^entry_/, '');
|
|
451
460
|
return {
|
|
452
|
-
id:
|
|
453
|
-
title: metadata.title ||
|
|
461
|
+
id: entryId,
|
|
462
|
+
title: metadata.title || entryId,
|
|
454
463
|
type: 'recipe',
|
|
455
464
|
kind: metadata.kind || 'pattern',
|
|
456
465
|
status: metadata.status || 'active',
|
|
457
466
|
score: Math.round(vr.score * 1000) / 1000,
|
|
458
467
|
};
|
|
459
468
|
});
|
|
469
|
+
// 按 entryId 去重 — 同一 Recipe 的多个 chunk 只保留最高分
|
|
470
|
+
results = this.#deduplicateByEntryId(results);
|
|
460
471
|
if (type !== 'all') {
|
|
461
472
|
results = results.filter((r) => {
|
|
462
473
|
if (type === 'rule') {
|
|
@@ -505,14 +516,20 @@ export class SearchEngine {
|
|
|
505
516
|
vectorResults = await this.vectorStore.query(queryEmbedding, limit * 2);
|
|
506
517
|
}
|
|
507
518
|
if (vectorResults && vectorResults.length > 0) {
|
|
508
|
-
let results = vectorResults.map((vr) =>
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
519
|
+
let results = vectorResults.map((vr) => {
|
|
520
|
+
const rawId = vr.id || '';
|
|
521
|
+
const entryId = vr.metadata?.entryId || rawId.replace(/^entry_/, '');
|
|
522
|
+
return {
|
|
523
|
+
id: entryId,
|
|
524
|
+
title: vr.metadata?.title || entryId,
|
|
525
|
+
type: 'recipe',
|
|
526
|
+
kind: vr.metadata?.kind || 'pattern',
|
|
527
|
+
status: vr.metadata?.status || 'active',
|
|
528
|
+
score: Math.round((vr.similarity || vr.score || 0) * 1000) / 1000,
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
// 按 entryId 去重
|
|
532
|
+
results = this.#deduplicateByEntryId(results);
|
|
516
533
|
if (type !== 'all') {
|
|
517
534
|
results = results.filter((r) => {
|
|
518
535
|
if (type === 'rule') {
|
|
@@ -542,6 +559,93 @@ export class SearchEngine {
|
|
|
542
559
|
return { items: this._scorerSearch(query, type, limit), actualMode: 'weighted' };
|
|
543
560
|
}
|
|
544
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* 按 entryId 去重 — 同一 Recipe 的多个 chunk 只保留最高分的
|
|
564
|
+
* 解决向量搜索返回同一条目的多个 chunk 浪费结果位的问题
|
|
565
|
+
*/
|
|
566
|
+
#deduplicateByEntryId(items) {
|
|
567
|
+
const seen = new Map();
|
|
568
|
+
for (const item of items) {
|
|
569
|
+
const existing = seen.get(item.id);
|
|
570
|
+
if (!existing || (item.score ?? 0) > (existing.score ?? 0)) {
|
|
571
|
+
seen.set(item.id, item);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return [...seen.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 评估 weighted 搜索结果的 confidence,决定是否需要语义搜索
|
|
578
|
+
* 返回 0-100 的分数,>= 60 跳过语义
|
|
579
|
+
*/
|
|
580
|
+
#computeWeightedConfidence(query, items, requestedLimit) {
|
|
581
|
+
let score = 0;
|
|
582
|
+
// ── 结果质量信号 ──
|
|
583
|
+
// FieldWeightedScorer 分数范围约 0-20,归一化后判断
|
|
584
|
+
const topScore = items[0]?.score ?? 0;
|
|
585
|
+
const secondScore = items[1]?.score ?? 0;
|
|
586
|
+
// top1 与 top2 分差大 → 明确命中
|
|
587
|
+
if (items.length >= 2 && topScore > 0) {
|
|
588
|
+
const relativeGap = (topScore - secondScore) / topScore;
|
|
589
|
+
if (relativeGap > 0.3) {
|
|
590
|
+
score += 25;
|
|
591
|
+
}
|
|
592
|
+
else if (relativeGap > 0.15) {
|
|
593
|
+
score += 15;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// title/trigger 匹配(子串级别)
|
|
597
|
+
const lq = query.toLowerCase();
|
|
598
|
+
const matchLevel = items.slice(0, 3).reduce((best, it) => {
|
|
599
|
+
const t = (it.title || '').toLowerCase();
|
|
600
|
+
const tr = (it.trigger || '').toLowerCase();
|
|
601
|
+
if (t === lq || tr === lq || tr === `@${lq}`) {
|
|
602
|
+
return Math.max(best, 3); // 完全匹配
|
|
603
|
+
}
|
|
604
|
+
if (t.includes(lq) || tr.includes(lq)) {
|
|
605
|
+
return Math.max(best, 2); // 子串匹配
|
|
606
|
+
}
|
|
607
|
+
if (lq.includes(t) && t.length > 3) {
|
|
608
|
+
return Math.max(best, 1); // 反向包含
|
|
609
|
+
}
|
|
610
|
+
return best;
|
|
611
|
+
}, 0);
|
|
612
|
+
if (matchLevel === 3) {
|
|
613
|
+
score += 50;
|
|
614
|
+
}
|
|
615
|
+
else if (matchLevel === 2) {
|
|
616
|
+
score += 35;
|
|
617
|
+
}
|
|
618
|
+
else if (matchLevel === 1) {
|
|
619
|
+
score += 15;
|
|
620
|
+
}
|
|
621
|
+
// 代码术语检测(CamelCase、snake_case、@trigger)
|
|
622
|
+
if (/^[A-Z][a-zA-Z0-9]+$/.test(query) ||
|
|
623
|
+
/^[a-z]+(_[a-z]+)+$/.test(query) ||
|
|
624
|
+
query.startsWith('@')) {
|
|
625
|
+
score += 25;
|
|
626
|
+
}
|
|
627
|
+
// 候选充足
|
|
628
|
+
if (items.length >= requestedLimit) {
|
|
629
|
+
score += 10;
|
|
630
|
+
}
|
|
631
|
+
// ── 查询特征信号(降低 confidence → 倾向调用语义)──
|
|
632
|
+
// 中文自然语言疑问句
|
|
633
|
+
if (/[如怎什为何哪]么?|是否|有没有|都有哪些|应该|需要/.test(query)) {
|
|
634
|
+
score -= 40;
|
|
635
|
+
}
|
|
636
|
+
// 英文自然语言问句
|
|
637
|
+
if (/^(how|what|why|when|where|which|can|does|is|should)\b/i.test(query)) {
|
|
638
|
+
score -= 40;
|
|
639
|
+
}
|
|
640
|
+
// 较长查询(可能是描述性语句)
|
|
641
|
+
if (query.length > 20) {
|
|
642
|
+
score -= 20;
|
|
643
|
+
}
|
|
644
|
+
else if (query.length > 10) {
|
|
645
|
+
score -= 10;
|
|
646
|
+
}
|
|
647
|
+
return Math.max(0, Math.min(100, score));
|
|
648
|
+
}
|
|
545
649
|
/**
|
|
546
650
|
* 补充详细字段(content / description / trigger / delivery 字段)— 批量 IN 查询
|
|
547
651
|
* 用于向量搜索结果与 FieldWeighted 结果的一致性
|
|
@@ -108,15 +108,30 @@ export class PrimeSearchPipeline {
|
|
|
108
108
|
const autoPromises = autoQueries.map((q) => this.#search
|
|
109
109
|
.search(q, { mode: 'auto', limit: 8, rank: false, context })
|
|
110
110
|
.catch(() => ({ items: [] })));
|
|
111
|
+
// Semantic-mode search for primary query — ensures semantic is always
|
|
112
|
+
// part of RRF fusion even when auto mode skips it (confidence ≥ 60)
|
|
113
|
+
const semanticPromise = autoQueries[0]
|
|
114
|
+
? this.#search
|
|
115
|
+
.search(autoQueries[0], { mode: 'semantic', limit: 6, rank: false })
|
|
116
|
+
.catch(() => ({ items: [] }))
|
|
117
|
+
: Promise.resolve({ items: [] });
|
|
111
118
|
// Keyword-mode searches (raw FWS scores — for cross-language synonym matching)
|
|
112
119
|
const kwPromises = keywordQueries.map((q) => this.#search
|
|
113
120
|
.search(q, { mode: 'keyword', limit: 8, rank: false })
|
|
114
121
|
.catch(() => ({ items: [] })));
|
|
115
|
-
const [autoResponses, kwResponses] = await Promise.all([
|
|
122
|
+
const [autoResponses, kwResponses, semanticResponse] = await Promise.all([
|
|
116
123
|
Promise.all(autoPromises),
|
|
117
124
|
Promise.all(kwPromises),
|
|
125
|
+
semanticPromise,
|
|
118
126
|
]);
|
|
119
|
-
|
|
127
|
+
// Merge: auto + semantic + keyword
|
|
128
|
+
const semanticItems = (semanticResponse.items ||
|
|
129
|
+
[]);
|
|
130
|
+
const allResponses = [
|
|
131
|
+
...autoResponses,
|
|
132
|
+
...(semanticItems.length > 0 ? [semanticResponse] : []),
|
|
133
|
+
...kwResponses,
|
|
134
|
+
];
|
|
120
135
|
// Single-query shortcut: preserve original scores from search engine.
|
|
121
136
|
// RRF is pointless with one response — it just converts rank to score,
|
|
122
137
|
// discarding the magnitude information from BM25/CoarseRanker.
|
|
@@ -201,13 +201,18 @@ export class VectorService {
|
|
|
201
201
|
}
|
|
202
202
|
const { topK = 10, filter = null, minScore = 0 } = opts;
|
|
203
203
|
try {
|
|
204
|
+
const t0 = performance.now();
|
|
204
205
|
const embedResult = await this.#embedProvider.embed(query);
|
|
206
|
+
const tEmbed = performance.now();
|
|
205
207
|
const queryVector = Array.isArray(embedResult[0]) ? embedResult[0] : embedResult;
|
|
206
|
-
|
|
208
|
+
const results = await this.#vectorStore.searchVector(queryVector, {
|
|
207
209
|
topK,
|
|
208
210
|
filter,
|
|
209
211
|
minScore,
|
|
210
212
|
});
|
|
213
|
+
const tHnsw = performance.now();
|
|
214
|
+
this.#logger.info(`[VectorService] search: embed=${Math.round(tEmbed - t0)}ms hnsw=${Math.round(tHnsw - tEmbed)}ms total=${Math.round(tHnsw - t0)}ms results=${results.length}`);
|
|
215
|
+
return results;
|
|
211
216
|
}
|
|
212
217
|
catch (err) {
|
|
213
218
|
this.#logger.warn('[VectorService] search failed', {
|
|
@@ -240,6 +245,7 @@ export class VectorService {
|
|
|
240
245
|
// Embed query — circuit breaker skips embed after repeated failures
|
|
241
246
|
let queryVector = null;
|
|
242
247
|
const circuitOpen = Date.now() < this.#embedCircuitOpenUntil;
|
|
248
|
+
const tEmbedStart = performance.now();
|
|
243
249
|
if (circuitOpen) {
|
|
244
250
|
this.#logger.debug('[VectorService] embed circuit open, skipping embed');
|
|
245
251
|
}
|
|
@@ -267,12 +273,15 @@ export class VectorService {
|
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
}
|
|
276
|
+
const tEmbedEnd = performance.now();
|
|
270
277
|
try {
|
|
271
278
|
const fused = await this.#hybridRetriever.search(query, queryVector, {
|
|
272
279
|
topK,
|
|
273
280
|
alpha,
|
|
274
281
|
sparseSearchFn: sparseSearchFn ?? undefined,
|
|
275
282
|
});
|
|
283
|
+
const tFuseEnd = performance.now();
|
|
284
|
+
this.#logger.info(`[VectorService] hybridSearch: embed=${Math.round(tEmbedEnd - tEmbedStart)}ms fuse=${Math.round(tFuseEnd - tEmbedEnd)}ms total=${Math.round(tFuseEnd - tEmbedStart)}ms hasVector=${!!queryVector} results=${fused.length} alpha=${alpha}`);
|
|
276
285
|
return fused.map((r) => ({
|
|
277
286
|
id: r.id || '',
|
|
278
287
|
score: r.score || 0,
|
|
@@ -161,5 +161,17 @@ export declare class LanguageService {
|
|
|
161
161
|
discovererIds?: string[];
|
|
162
162
|
maxDepth?: number;
|
|
163
163
|
}): unknown[];
|
|
164
|
+
/**
|
|
165
|
+
* 判定文件路径是否为测试文件
|
|
166
|
+
*
|
|
167
|
+
* 两层判定:
|
|
168
|
+
* 1. 语言特定的文件名模式(_test.go, .test.ts, test_*.py 等)
|
|
169
|
+
* 2. 通用测试目录模式(test/, tests/, __tests__/, spec/ 等)
|
|
170
|
+
*
|
|
171
|
+
* @param filePath 文件路径(相对或绝对均可)
|
|
172
|
+
* @param [language] 已知语言 ID,省略时从扩展名推断
|
|
173
|
+
* @returns 是否为测试文件
|
|
174
|
+
*/
|
|
175
|
+
static isTestFile(filePath: string, language?: string): boolean;
|
|
164
176
|
}
|
|
165
177
|
export default LanguageService;
|
|
@@ -275,6 +275,10 @@ const SCAN_SKIP_DIRS = Object.freeze(new Set([
|
|
|
275
275
|
'.cargo',
|
|
276
276
|
]));
|
|
277
277
|
// ═══════════════════════════════════════════════════════════
|
|
278
|
+
// 7.5) 通用测试目录模式(路径中包含典型测试目录名)
|
|
279
|
+
// ═══════════════════════════════════════════════════════════
|
|
280
|
+
const TEST_DIR_PATTERN = /(?:^|[/\\])(?:tests?|__tests__|spec|__mocks__|testdata|test_driver|integration_test|e2e)[/\\]/;
|
|
281
|
+
// ═══════════════════════════════════════════════════════════
|
|
278
282
|
// Lazy caches
|
|
279
283
|
// ═══════════════════════════════════════════════════════════
|
|
280
284
|
let _sourceExtRegex = null;
|
|
@@ -567,6 +571,14 @@ export class LanguageService {
|
|
|
567
571
|
langSet.add(lang);
|
|
568
572
|
}
|
|
569
573
|
}
|
|
574
|
+
// 启发式: node 与其他生态共存时,JS/TS 通常只是构建工具,去掉
|
|
575
|
+
if (nonGeneric.length > 1 && nonGeneric.includes('node')) {
|
|
576
|
+
const hasOther = nonGeneric.some((e) => e !== 'node');
|
|
577
|
+
if (hasOther) {
|
|
578
|
+
langSet.delete('javascript');
|
|
579
|
+
langSet.delete('typescript');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
570
582
|
if (langSet.size > 0) {
|
|
571
583
|
return [...langSet];
|
|
572
584
|
}
|
|
@@ -647,5 +659,78 @@ export class LanguageService {
|
|
|
647
659
|
}
|
|
648
660
|
return [...langSet];
|
|
649
661
|
}
|
|
662
|
+
// ═══════════════════════════════════════════════════════════
|
|
663
|
+
// 9) 测试文件判定 — 统一入口
|
|
664
|
+
// ═══════════════════════════════════════════════════════════
|
|
665
|
+
/**
|
|
666
|
+
* 判定文件路径是否为测试文件
|
|
667
|
+
*
|
|
668
|
+
* 两层判定:
|
|
669
|
+
* 1. 语言特定的文件名模式(_test.go, .test.ts, test_*.py 等)
|
|
670
|
+
* 2. 通用测试目录模式(test/, tests/, __tests__/, spec/ 等)
|
|
671
|
+
*
|
|
672
|
+
* @param filePath 文件路径(相对或绝对均可)
|
|
673
|
+
* @param [language] 已知语言 ID,省略时从扩展名推断
|
|
674
|
+
* @returns 是否为测试文件
|
|
675
|
+
*/
|
|
676
|
+
static isTestFile(filePath, language) {
|
|
677
|
+
if (!filePath) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const name = filePath.split(/[/\\]/).pop() || '';
|
|
681
|
+
const lang = language || LanguageService.inferLang(name);
|
|
682
|
+
// ── 1. 语言特定的文件名模式 ──
|
|
683
|
+
switch (lang) {
|
|
684
|
+
case 'go':
|
|
685
|
+
if (name.endsWith('_test.go')) {
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
break;
|
|
689
|
+
case 'swift':
|
|
690
|
+
if (name.endsWith('Tests.swift') || name.endsWith('Test.swift')) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
break;
|
|
694
|
+
case 'rust':
|
|
695
|
+
if (name.endsWith('_test.rs') || name.startsWith('test_')) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
case 'javascript':
|
|
700
|
+
case 'typescript':
|
|
701
|
+
if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|mts)$/.test(name)) {
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
break;
|
|
705
|
+
case 'python':
|
|
706
|
+
if (name.startsWith('test_') || name.endsWith('_test.py')) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
case 'java':
|
|
711
|
+
case 'kotlin':
|
|
712
|
+
if (name.endsWith('Test.java') ||
|
|
713
|
+
name.endsWith('Test.kt') ||
|
|
714
|
+
name.endsWith('Tests.java') ||
|
|
715
|
+
name.endsWith('Tests.kt')) {
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
break;
|
|
719
|
+
case 'ruby':
|
|
720
|
+
if (name.endsWith('_spec.rb') || name.endsWith('_test.rb') || name.startsWith('test_')) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
break;
|
|
724
|
+
case 'dart':
|
|
725
|
+
if (name.endsWith('_test.dart')) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
break;
|
|
729
|
+
default:
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
// ── 2. 通用测试目录模式 ──
|
|
733
|
+
return TEST_DIR_PATTERN.test(filePath);
|
|
734
|
+
}
|
|
650
735
|
}
|
|
651
736
|
export default LanguageService;
|
|
@@ -303,6 +303,10 @@ export declare const AiEnvConfigBody: z.ZodObject<{
|
|
|
303
303
|
model: z.ZodOptional<z.ZodString>;
|
|
304
304
|
apiKey: z.ZodOptional<z.ZodString>;
|
|
305
305
|
proxy: z.ZodOptional<z.ZodString>;
|
|
306
|
+
embedProvider: z.ZodOptional<z.ZodString>;
|
|
307
|
+
embedModel: z.ZodOptional<z.ZodString>;
|
|
308
|
+
embedBaseUrl: z.ZodOptional<z.ZodString>;
|
|
309
|
+
embedApiKey: z.ZodOptional<z.ZodString>;
|
|
306
310
|
}, z.core.$strip>;
|
|
307
311
|
export declare const ExtractPathBody: z.ZodObject<{
|
|
308
312
|
relativePath: z.ZodString;
|
|
@@ -303,6 +303,10 @@ export const AiEnvConfigBody = z.object({
|
|
|
303
303
|
model: z.string().optional(),
|
|
304
304
|
apiKey: z.string().optional(),
|
|
305
305
|
proxy: z.string().optional(),
|
|
306
|
+
embedProvider: z.string().optional(),
|
|
307
|
+
embedModel: z.string().optional(),
|
|
308
|
+
embedBaseUrl: z.string().optional(),
|
|
309
|
+
embedApiKey: z.string().optional(),
|
|
306
310
|
});
|
|
307
311
|
// ═══ Extract Routes ══════════════════════════════
|
|
308
312
|
export const ExtractPathBody = z.object({
|
package/package.json
CHANGED
|
Binary file
|