autosnippet 3.3.2 → 3.3.4
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 +8 -4
- package/dist/bin/cli.js +27 -1
- package/dist/lib/cli/KnowledgeSyncService.d.ts +26 -0
- package/dist/lib/cli/KnowledgeSyncService.js +33 -1
- package/dist/lib/external/mcp/handlers/browse.d.ts +1 -0
- package/dist/lib/external/mcp/handlers/browse.js +2 -1
- package/dist/lib/external/mcp/handlers/consolidated.d.ts +1 -0
- package/dist/lib/external/mcp/handlers/panorama.d.ts +11 -11
- package/dist/lib/external/mcp/handlers/panorama.js +20 -20
- package/dist/lib/external/mcp/handlers/system.d.ts +1 -1
- package/dist/lib/external/mcp/handlers/task.js +38 -15
- package/dist/lib/external/mcp/tools.d.ts +12 -12
- package/dist/lib/external/mcp/tools.js +120 -118
- package/dist/lib/http/middleware/validate.js +7 -3
- package/dist/lib/infrastructure/database/drizzle/schema.d.ts +100 -0
- package/dist/lib/infrastructure/database/drizzle/schema.js +10 -0
- package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.d.ts +9 -0
- package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.js +24 -0
- package/dist/lib/infrastructure/vector/HnswVectorAdapter.js +18 -2
- package/dist/lib/injection/ServiceContainer.js +2 -0
- package/dist/lib/injection/modules/KnowledgeModule.d.ts +5 -0
- package/dist/lib/injection/modules/KnowledgeModule.js +80 -0
- package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +45 -0
- package/dist/lib/service/bootstrap/UiStartupTasks.js +101 -0
- package/dist/lib/service/evolution/ConsolidationAdvisor.js +9 -9
- package/dist/lib/service/evolution/ContradictionDetector.js +2 -2
- package/dist/lib/service/evolution/RedundancyAnalyzer.js +2 -2
- package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +68 -0
- package/dist/lib/service/knowledge/SourceRefReconciler.js +309 -0
- package/dist/lib/service/panorama/PanoramaService.d.ts +18 -1
- package/dist/lib/service/panorama/PanoramaService.js +148 -5
- package/dist/lib/service/search/BM25Scorer.d.ts +2 -2
- package/dist/lib/service/search/CoarseRanker.d.ts +7 -6
- package/dist/lib/service/search/CoarseRanker.js +11 -10
- package/dist/lib/service/search/FieldWeightedScorer.d.ts +81 -0
- package/dist/lib/service/search/FieldWeightedScorer.js +318 -0
- package/dist/lib/service/search/MultiSignalRanker.d.ts +2 -2
- package/dist/lib/service/search/MultiSignalRanker.js +1 -1
- package/dist/lib/service/search/SearchEngine.d.ts +8 -7
- package/dist/lib/service/search/SearchEngine.js +59 -10
- package/dist/lib/service/search/SearchTypes.d.ts +23 -3
- package/dist/lib/service/search/SearchTypes.js +6 -1
- package/dist/lib/service/task/IntentExtractor.d.ts +11 -1
- package/dist/lib/service/task/IntentExtractor.js +137 -3
- package/dist/lib/service/task/PrimeSearchPipeline.js +95 -25
- package/dist/lib/service/vector/VectorService.d.ts +3 -0
- package/dist/lib/service/vector/VectorService.js +38 -4
- package/dist/lib/shared/schemas/mcp-tools.d.ts +1 -0
- package/dist/lib/shared/schemas/mcp-tools.js +5 -1
- package/package.json +1 -1
- package/skills/autosnippet-create/SKILL.md +98 -89
- package/skills/autosnippet-devdocs/SKILL.md +55 -60
- package/templates/guard-ci.yml +2 -2
- package/templates/instructions/conventions.md +4 -2
- package/templates/recipes-setup/_template.md +39 -39
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module service/task/IntentExtractor
|
|
8
8
|
*/
|
|
9
|
+
import { tokenize } from '#service/search/tokenizer.js';
|
|
9
10
|
// ── Universal Patterns (language-agnostic) ──────────
|
|
10
11
|
const UNIVERSAL_PATTERNS = [
|
|
11
12
|
/\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b/g, // CamelCase
|
|
@@ -29,6 +30,81 @@ const LANG_MAP = {
|
|
|
29
30
|
java: 'java',
|
|
30
31
|
kt: 'kotlin',
|
|
31
32
|
};
|
|
33
|
+
// ── Cross-Language Synonym Groups ───────────────────
|
|
34
|
+
// Each group contains EN morphological variants + CN equivalents.
|
|
35
|
+
// Used to expand queries so English terms match Chinese recipe fields (and vice versa).
|
|
36
|
+
const SYNONYM_GROUPS = [
|
|
37
|
+
// Design patterns & DI
|
|
38
|
+
['inject', 'injection', '注入'],
|
|
39
|
+
['construct', 'constructor', '构造器', '构造函数'],
|
|
40
|
+
['depend', 'dependency', 'dependencies', '依赖'],
|
|
41
|
+
['protocol', '协议'],
|
|
42
|
+
['interface', '接口'],
|
|
43
|
+
['pattern', '模式'],
|
|
44
|
+
['factory', '工厂'],
|
|
45
|
+
['singleton', '单例'],
|
|
46
|
+
['delegate', '代理', '委托'],
|
|
47
|
+
['observe', 'observer', '观察者'],
|
|
48
|
+
['subscribe', 'subscription', '订阅'],
|
|
49
|
+
['repository', 'repo', '仓库'],
|
|
50
|
+
// Architecture
|
|
51
|
+
['module', '模块'],
|
|
52
|
+
['architect', 'architecture', '架构'],
|
|
53
|
+
['route', 'router', 'routing', '路由'],
|
|
54
|
+
['middleware', '中间件'],
|
|
55
|
+
['component', '组件'],
|
|
56
|
+
['lifecycle', '生命周期'],
|
|
57
|
+
['layer', '分层', '层'],
|
|
58
|
+
// Language features
|
|
59
|
+
['generic', 'generics', '泛型'],
|
|
60
|
+
['closure', '闭包'],
|
|
61
|
+
['callback', '回调'],
|
|
62
|
+
['extend', 'extension', '扩展'],
|
|
63
|
+
['inherit', 'inheritance', '继承'],
|
|
64
|
+
['abstract', 'abstraction', '抽象'],
|
|
65
|
+
['encapsulate', 'encapsulation', '封装'],
|
|
66
|
+
['polymorph', 'polymorphism', '多态'],
|
|
67
|
+
['implement', 'implementation', '实现'],
|
|
68
|
+
// Concurrency
|
|
69
|
+
['async', 'asynchronous', '异步'],
|
|
70
|
+
['sync', 'synchronous', '同步'],
|
|
71
|
+
['thread', 'threading', '线程'],
|
|
72
|
+
['concur', 'concurrency', '并发'],
|
|
73
|
+
// Memory management
|
|
74
|
+
['memory', '内存'],
|
|
75
|
+
['leak', 'leakage', '泄漏'],
|
|
76
|
+
['weak', '弱引用'],
|
|
77
|
+
['retain', '持有', '保留'],
|
|
78
|
+
['release', '释放'],
|
|
79
|
+
['reference', '引用'],
|
|
80
|
+
// Common concepts
|
|
81
|
+
['network', '网络'],
|
|
82
|
+
['cache', 'caching', '缓存'],
|
|
83
|
+
['persist', 'persistence', '持久化'],
|
|
84
|
+
['serialize', 'serialization', '序列化'],
|
|
85
|
+
['validate', 'validation', '校验', '验证'],
|
|
86
|
+
['authenticate', 'authentication', '认证'],
|
|
87
|
+
['authorize', 'authorization', '授权'],
|
|
88
|
+
['config', 'configuration', '配置'],
|
|
89
|
+
['navigate', 'navigation', '导航'],
|
|
90
|
+
['animate', 'animation', '动画'],
|
|
91
|
+
['layout', '布局'],
|
|
92
|
+
['render', 'rendering', '渲染'],
|
|
93
|
+
['responsive', '响应式'],
|
|
94
|
+
['state', '状态'],
|
|
95
|
+
['toast', '提示'],
|
|
96
|
+
['error', '错误'],
|
|
97
|
+
['handle', 'handler', '处理'],
|
|
98
|
+
['service', '服务'],
|
|
99
|
+
['test', 'testing', '测试'],
|
|
100
|
+
];
|
|
101
|
+
/** Lookup: lowercased term → synonym expansions (excluding the term itself) */
|
|
102
|
+
const SYNONYM_LOOKUP = new Map();
|
|
103
|
+
for (const group of SYNONYM_GROUPS) {
|
|
104
|
+
for (const term of group) {
|
|
105
|
+
SYNONYM_LOOKUP.set(term.toLowerCase(), group.filter((t) => t !== term));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
32
108
|
// ── Public API ──────────────────────────────────────
|
|
33
109
|
/**
|
|
34
110
|
* Extract intent signals from user query and active file.
|
|
@@ -36,11 +112,13 @@ const LANG_MAP = {
|
|
|
36
112
|
*/
|
|
37
113
|
export function extract(userQuery, activeFile, language, termOpts) {
|
|
38
114
|
const queries = buildQueries(userQuery, activeFile, termOpts);
|
|
115
|
+
const keywordQueries = buildKeywordQueries(userQuery);
|
|
39
116
|
const inferredLang = language || (activeFile ? inferLanguage(activeFile) : null);
|
|
40
117
|
const module = activeFile ? inferFileContext(activeFile) : null;
|
|
41
118
|
const scenario = classifyScenario(userQuery);
|
|
42
119
|
return {
|
|
43
120
|
queries,
|
|
121
|
+
keywordQueries,
|
|
44
122
|
language: inferredLang,
|
|
45
123
|
module,
|
|
46
124
|
scenario,
|
|
@@ -49,14 +127,28 @@ export function extract(userQuery, activeFile, language, termOpts) {
|
|
|
49
127
|
}
|
|
50
128
|
/**
|
|
51
129
|
* Build multi-query set from user query + active file.
|
|
52
|
-
* Q1: raw query, Q2: extracted tech terms, Q3: file context.
|
|
130
|
+
* Q1: raw query, Q2: extracted tech terms, Q3: file context, Q4: synonym focus.
|
|
131
|
+
* Q1 is enriched with cross-language synonyms to bridge EN↔CJK matching.
|
|
132
|
+
* Q4 (long queries only): synonym expansion as a separate focused query
|
|
133
|
+
* to prevent BM25 dilution in verbose natural language inputs.
|
|
53
134
|
*/
|
|
54
135
|
export function buildQueries(userQuery, activeFile, termOpts) {
|
|
55
|
-
|
|
136
|
+
// Enrich raw query with cross-language synonyms
|
|
137
|
+
const synonyms = expandWithSynonyms(userQuery);
|
|
138
|
+
const enrichedQuery = synonyms ? `${userQuery} ${synonyms}` : userQuery;
|
|
139
|
+
const queries = [enrichedQuery];
|
|
56
140
|
const terms = extractTechTerms(userQuery, termOpts);
|
|
57
141
|
if (terms.length > 0) {
|
|
58
142
|
queries.push(terms.join(' '));
|
|
59
143
|
}
|
|
144
|
+
// Q4: For long queries (> 50 chars), add cross-language synonyms as a
|
|
145
|
+
// separate focused query. In long sentences, synonym terms appended to Q1
|
|
146
|
+
// get diluted by common words ("ViewController", "ViewModel"), causing
|
|
147
|
+
// BM25 to miss the user's actual intent. A short focused query matches
|
|
148
|
+
// domain-specific terms (e.g. "singleton 单例 inject 注入") directly.
|
|
149
|
+
if (synonyms && userQuery.length > 50) {
|
|
150
|
+
queries.push(synonyms);
|
|
151
|
+
}
|
|
60
152
|
if (activeFile) {
|
|
61
153
|
const ctx = inferFileContext(activeFile);
|
|
62
154
|
if (ctx) {
|
|
@@ -65,6 +157,14 @@ export function buildQueries(userQuery, activeFile, termOpts) {
|
|
|
65
157
|
}
|
|
66
158
|
return queries;
|
|
67
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Build keyword-mode queries for cross-language synonym matching.
|
|
162
|
+
* Uses keyword mode to preserve raw FWS scores without CoarseRanker semantic normalization.
|
|
163
|
+
*/
|
|
164
|
+
export function buildKeywordQueries(userQuery) {
|
|
165
|
+
const expanded = expandWithSynonyms(userQuery);
|
|
166
|
+
return expanded ? [expanded] : [];
|
|
167
|
+
}
|
|
68
168
|
/**
|
|
69
169
|
* Extract tech terms from query using universal patterns + dynamic project prefixes.
|
|
70
170
|
*/
|
|
@@ -120,7 +220,7 @@ export function inferLanguage(filePath) {
|
|
|
120
220
|
*/
|
|
121
221
|
export function classifyScenario(userQuery) {
|
|
122
222
|
const q = userQuery.toLowerCase();
|
|
123
|
-
if (/帮我[加写做实现创建]|implement|add|create|新[增加建]
|
|
223
|
+
if (/帮我[加写做实现创建]|implement|add|create|新[增加建]|添加|修改|删除|实现|开发|编写|创建|初始化/.test(q)) {
|
|
124
224
|
return 'generate';
|
|
125
225
|
}
|
|
126
226
|
if (/检查|review|lint|合规|违规|guard|规[则范]/.test(q)) {
|
|
@@ -132,6 +232,40 @@ export function classifyScenario(userQuery) {
|
|
|
132
232
|
return 'search';
|
|
133
233
|
}
|
|
134
234
|
// ── Internal Helpers ────────────────────────────────
|
|
235
|
+
/**
|
|
236
|
+
* Expand query tokens with cross-language synonyms.
|
|
237
|
+
* Tokenizes query, looks up each token in the synonym table,
|
|
238
|
+
* returns a query string of synonym expansions for cross-language matching.
|
|
239
|
+
*
|
|
240
|
+
* Strategy: per-token cross-script expansion. Each token's script is checked
|
|
241
|
+
* individually, and only synonyms in the OPPOSITE script are added.
|
|
242
|
+
* This correctly handles mixed EN/CJK queries (e.g. "在 module 里用 singleton")
|
|
243
|
+
* where both EN→CJK and CJK→EN expansions are needed.
|
|
244
|
+
*/
|
|
245
|
+
function expandWithSynonyms(query) {
|
|
246
|
+
const tokens = tokenize(query);
|
|
247
|
+
const crossScriptTerms = new Set();
|
|
248
|
+
const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf]/;
|
|
249
|
+
for (const token of tokens) {
|
|
250
|
+
const synonyms = SYNONYM_LOOKUP.get(token.toLowerCase());
|
|
251
|
+
if (!synonyms) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// Determine THIS token's script, not the whole query's
|
|
255
|
+
const tokenIsCJK = CJK_RE.test(token);
|
|
256
|
+
for (const syn of synonyms) {
|
|
257
|
+
const synIsCJK = CJK_RE.test(syn);
|
|
258
|
+
// Cross-script: EN token → add CJK synonyms; CJK token → add EN synonyms
|
|
259
|
+
if (tokenIsCJK !== synIsCJK) {
|
|
260
|
+
crossScriptTerms.add(syn);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (crossScriptTerms.size === 0) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return [...crossScriptTerms].slice(0, 16).join(' ');
|
|
268
|
+
}
|
|
135
269
|
function buildPrefixPattern(prefixes) {
|
|
136
270
|
if (prefixes.length === 0) {
|
|
137
271
|
return null;
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { slimSearchResult } from '#service/search/SearchTypes.js';
|
|
10
10
|
// ── Constants ───────────────────────────────────────
|
|
11
|
-
|
|
11
|
+
/** Absolute minimum score — items below this are definitely noise */
|
|
12
|
+
const MIN_SCORE_THRESHOLD = 0.3;
|
|
13
|
+
/** Relative threshold — items scoring below this fraction of the best result are dropped */
|
|
14
|
+
const RELATIVE_SCORE_RATIO = 0.15;
|
|
15
|
+
/** Gap ratio — if score drops by more than this factor from the previous item, truncate */
|
|
16
|
+
const GAP_DROP_RATIO = 0.25;
|
|
12
17
|
// ── PrimeSearchPipeline ─────────────────────────────
|
|
13
18
|
export class PrimeSearchPipeline {
|
|
14
19
|
#search;
|
|
@@ -29,10 +34,10 @@ export class PrimeSearchPipeline {
|
|
|
29
34
|
intent: intent.scenario,
|
|
30
35
|
sessionHistory: this.#buildSessionHistory(),
|
|
31
36
|
};
|
|
32
|
-
// Multi-query parallel search
|
|
33
|
-
const allResults = await this.#multiQuerySearch(intent.queries, context);
|
|
34
|
-
//
|
|
35
|
-
const filtered = allResults
|
|
37
|
+
// Multi-query parallel search (auto mode + keyword mode for cross-language)
|
|
38
|
+
const allResults = await this.#multiQuerySearch(intent.queries, intent.keywordQueries ?? [], context);
|
|
39
|
+
// Quality filter: absolute threshold + relative-to-best + score gap detection
|
|
40
|
+
const filtered = this.#qualityFilter(allResults);
|
|
36
41
|
if (filtered.length === 0) {
|
|
37
42
|
return null;
|
|
38
43
|
}
|
|
@@ -62,32 +67,97 @@ export class PrimeSearchPipeline {
|
|
|
62
67
|
}
|
|
63
68
|
// ── Private ───────────────────────────────────────
|
|
64
69
|
/**
|
|
65
|
-
*
|
|
70
|
+
* Quality filter: absolute threshold + relative-to-best + score gap detection.
|
|
71
|
+
* Expects items sorted by score descending.
|
|
66
72
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
#qualityFilter(items) {
|
|
74
|
+
if (items.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const maxScore = items[0]?.score ?? 0;
|
|
78
|
+
const effectiveThreshold = Math.max(MIN_SCORE_THRESHOLD, maxScore * RELATIVE_SCORE_RATIO);
|
|
79
|
+
const result = [];
|
|
80
|
+
let prevScore = maxScore;
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
const score = item.score;
|
|
83
|
+
if (score < effectiveThreshold) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// Gap detection: if score drops sharply from previous item, stop
|
|
87
|
+
if (result.length > 0 && score < prevScore * GAP_DROP_RATIO) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
result.push(item);
|
|
91
|
+
prevScore = score;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Multi-query parallel search with optional Reciprocal Rank Fusion (RRF).
|
|
97
|
+
*
|
|
98
|
+
* Single-query: preserves original search engine scores (BM25/CoarseRanker).
|
|
99
|
+
* Multi-query: uses RRF to fuse results, but weights by original score to
|
|
100
|
+
* retain magnitude information.
|
|
101
|
+
*/
|
|
102
|
+
async #multiQuerySearch(autoQueries, keywordQueries, context) {
|
|
103
|
+
// Auto-mode searches (BM25 without CoarseRanker ranking)
|
|
104
|
+
// Using rank: false preserves raw BM25/FWS score magnitude,
|
|
105
|
+
// which the quality filter needs for effective discrimination.
|
|
106
|
+
// CoarseRanker's max-normalization + freshness/popularity signals
|
|
107
|
+
// would cluster scores around 0.35–0.41, defeating the filter.
|
|
108
|
+
const autoPromises = autoQueries.map((q) => this.#search
|
|
109
|
+
.search(q, { mode: 'auto', limit: 8, rank: false, context })
|
|
110
|
+
.catch(() => ({ items: [] })));
|
|
111
|
+
// Keyword-mode searches (raw FWS scores — for cross-language synonym matching)
|
|
112
|
+
const kwPromises = keywordQueries.map((q) => this.#search
|
|
113
|
+
.search(q, { mode: 'keyword', limit: 8, rank: false })
|
|
75
114
|
.catch(() => ({ items: [] })));
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
const [autoResponses, kwResponses] = await Promise.all([
|
|
116
|
+
Promise.all(autoPromises),
|
|
117
|
+
Promise.all(kwPromises),
|
|
118
|
+
]);
|
|
119
|
+
const allResponses = [...autoResponses, ...kwResponses];
|
|
120
|
+
// Single-query shortcut: preserve original scores from search engine.
|
|
121
|
+
// RRF is pointless with one response — it just converts rank to score,
|
|
122
|
+
// discarding the magnitude information from BM25/CoarseRanker.
|
|
123
|
+
if (allResponses.length === 1) {
|
|
124
|
+
const items = (allResponses[0]?.items || []);
|
|
125
|
+
return items.map(slimSearchResult).sort((a, b) => b.score - a.score);
|
|
126
|
+
}
|
|
127
|
+
// Multi-query: Weighted RRF — RRF(d) = Σ origScore / (k + rank)
|
|
128
|
+
// Retains original score magnitude while still boosting cross-query overlap.
|
|
129
|
+
const RRF_K = 60;
|
|
130
|
+
const rrfScores = new Map();
|
|
131
|
+
const itemById = new Map();
|
|
132
|
+
for (const resp of allResponses) {
|
|
133
|
+
const items = (resp.items || []);
|
|
134
|
+
for (let rank = 0; rank < items.length; rank++) {
|
|
135
|
+
const raw = items[rank];
|
|
136
|
+
const origScore = Math.max(raw.score || 0, 0.01);
|
|
82
137
|
const item = slimSearchResult(raw);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
138
|
+
rrfScores.set(item.id, (rrfScores.get(item.id) ?? 0) + origScore / (RRF_K + rank));
|
|
139
|
+
// Keep the richest metadata version
|
|
140
|
+
if (!itemById.has(item.id)) {
|
|
141
|
+
itemById.set(item.id, item);
|
|
86
142
|
}
|
|
87
143
|
}
|
|
88
144
|
}
|
|
89
|
-
//
|
|
90
|
-
|
|
145
|
+
// Assign fused scores and sort
|
|
146
|
+
// Rescale: RRF_K division crushes scores to ~0.003–0.02 range,
|
|
147
|
+
// which falls below qualityFilter's MIN_SCORE_THRESHOLD (0.1).
|
|
148
|
+
// Multiply by RRF_K to restore original score magnitude.
|
|
149
|
+
// Effective formula: Σ origScore / (1 + rank/K), preserving magnitude
|
|
150
|
+
// while still giving a gentle rank-based discount.
|
|
151
|
+
const results = [];
|
|
152
|
+
for (const [id, rrfScore] of rrfScores) {
|
|
153
|
+
const item = itemById.get(id);
|
|
154
|
+
if (!item) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
item.score = Math.round(rrfScore * RRF_K * 1000) / 1000;
|
|
158
|
+
results.push(item);
|
|
159
|
+
}
|
|
160
|
+
return results.sort((a, b) => b.score - a.score);
|
|
91
161
|
}
|
|
92
162
|
/**
|
|
93
163
|
* Build sessionHistory for contextBoost (last 5 queries).
|
|
@@ -109,6 +109,9 @@ export declare class VectorService {
|
|
|
109
109
|
/**
|
|
110
110
|
* 混合搜索 (Dense + Sparse RRF 融合)
|
|
111
111
|
* 通过 HybridRetriever 执行向量 + BM25 关键词并行检索
|
|
112
|
+
*
|
|
113
|
+
* Embed 失败时优雅降级: 跳过 Dense 路, 仅用 Sparse 结果进行 RRF 融合,
|
|
114
|
+
* 避免因网络问题导致整个搜索返回空结果。
|
|
112
115
|
*/
|
|
113
116
|
hybridSearch(query: string, opts?: {
|
|
114
117
|
topK?: number;
|
|
@@ -26,6 +26,11 @@ export class VectorService {
|
|
|
26
26
|
#syncDebounceMs;
|
|
27
27
|
#logger = Logger.getInstance();
|
|
28
28
|
#initialized = false;
|
|
29
|
+
// ── Embed circuit breaker ──
|
|
30
|
+
#embedConsecutiveFailures = 0;
|
|
31
|
+
#embedCircuitOpenUntil = 0;
|
|
32
|
+
static #EMBED_CIRCUIT_THRESHOLD = 3;
|
|
33
|
+
static #EMBED_CIRCUIT_COOLDOWN_MS = 60_000;
|
|
29
34
|
constructor(config) {
|
|
30
35
|
this.#vectorStore = config.vectorStore;
|
|
31
36
|
this.#indexingPipeline = config.indexingPipeline;
|
|
@@ -211,6 +216,9 @@ export class VectorService {
|
|
|
211
216
|
/**
|
|
212
217
|
* 混合搜索 (Dense + Sparse RRF 融合)
|
|
213
218
|
* 通过 HybridRetriever 执行向量 + BM25 关键词并行检索
|
|
219
|
+
*
|
|
220
|
+
* Embed 失败时优雅降级: 跳过 Dense 路, 仅用 Sparse 结果进行 RRF 融合,
|
|
221
|
+
* 避免因网络问题导致整个搜索返回空结果。
|
|
214
222
|
*/
|
|
215
223
|
async hybridSearch(query, opts = {}) {
|
|
216
224
|
if (!this.#embedProvider) {
|
|
@@ -226,11 +234,37 @@ export class VectorService {
|
|
|
226
234
|
}));
|
|
227
235
|
}
|
|
228
236
|
const { topK = 10, alpha = 0.5, sparseSearchFn = null } = opts;
|
|
237
|
+
// Embed query — circuit breaker skips embed after repeated failures
|
|
238
|
+
let queryVector = null;
|
|
239
|
+
const circuitOpen = Date.now() < this.#embedCircuitOpenUntil;
|
|
240
|
+
if (circuitOpen) {
|
|
241
|
+
this.#logger.debug('[VectorService] embed circuit open, skipping embed');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
try {
|
|
245
|
+
const embedResult = await this.#embedProvider.embed(query);
|
|
246
|
+
queryVector = Array.isArray(embedResult[0])
|
|
247
|
+
? embedResult[0]
|
|
248
|
+
: embedResult;
|
|
249
|
+
this.#embedConsecutiveFailures = 0;
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
this.#embedConsecutiveFailures++;
|
|
253
|
+
if (this.#embedConsecutiveFailures >= VectorService.#EMBED_CIRCUIT_THRESHOLD) {
|
|
254
|
+
this.#embedCircuitOpenUntil = Date.now() + VectorService.#EMBED_CIRCUIT_COOLDOWN_MS;
|
|
255
|
+
this.#logger.warn('[VectorService] embed circuit OPEN — skipping embed for 60s', {
|
|
256
|
+
consecutiveFailures: this.#embedConsecutiveFailures,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.#logger.warn('[VectorService] embed failed, degrading to sparse-only', {
|
|
261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
262
|
+
failCount: this.#embedConsecutiveFailures,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
229
267
|
try {
|
|
230
|
-
const embedResult = await this.#embedProvider.embed(query);
|
|
231
|
-
const queryVector = Array.isArray(embedResult[0])
|
|
232
|
-
? embedResult[0]
|
|
233
|
-
: embedResult;
|
|
234
268
|
const fused = await this.#hybridRetriever.search(query, queryVector, {
|
|
235
269
|
topK,
|
|
236
270
|
alpha,
|
|
@@ -247,6 +247,7 @@ export declare const TaskInput: z.ZodObject<{
|
|
|
247
247
|
title: z.ZodOptional<z.ZodString>;
|
|
248
248
|
description: z.ZodOptional<z.ZodString>;
|
|
249
249
|
id: z.ZodOptional<z.ZodString>;
|
|
250
|
+
taskId: z.ZodOptional<z.ZodString>;
|
|
250
251
|
reason: z.ZodOptional<z.ZodString>;
|
|
251
252
|
rationale: z.ZodOptional<z.ZodString>;
|
|
252
253
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -235,7 +235,11 @@ export const TaskInput = z.object({
|
|
|
235
235
|
.describe('prime=加载知识上下文 | create=创建任务锚点 | close=完成+Guard | fail=放弃 | record_decision=记录用户偏好'),
|
|
236
236
|
title: z.string().optional().describe('Task or decision title (create / record_decision)'),
|
|
237
237
|
description: z.string().optional().describe('Decision description (record_decision)'),
|
|
238
|
-
id: z
|
|
238
|
+
id: z
|
|
239
|
+
.string()
|
|
240
|
+
.optional()
|
|
241
|
+
.describe('Task ID (close / fail). Optional if a task was created in the current session.'),
|
|
242
|
+
taskId: z.string().optional().describe('Alias for id (accepted for convenience)'),
|
|
239
243
|
reason: z.string().optional().describe('Close reason or fail reason'),
|
|
240
244
|
rationale: z.string().optional().describe('Decision rationale (record_decision)'),
|
|
241
245
|
tags: z.array(z.string()).optional().describe('Decision tags (record_decision)'),
|