autosnippet 2.18.0 → 2.19.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.
Files changed (29) hide show
  1. package/dashboard/dist/assets/{icons-C6kshpB1.js → icons-C7FN32VL.js} +1 -1
  2. package/dashboard/dist/assets/index-D8dCXLzr.js +129 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/lib/external/ai/AiProvider.js +42 -11
  5. package/lib/external/ai/providers/ClaudeProvider.js +4 -2
  6. package/lib/external/ai/providers/GoogleGeminiProvider.js +66 -8
  7. package/lib/external/ai/providers/OpenAiProvider.js +48 -2
  8. package/lib/external/mcp/handlers/bootstrap.js +1 -2
  9. package/lib/http/HttpServer.js +4 -0
  10. package/lib/http/routes/candidates.js +405 -0
  11. package/lib/http/routes/search.js +113 -0
  12. package/lib/infrastructure/vector/Chunker.js +3 -8
  13. package/lib/infrastructure/vector/JsonVectorAdapter.js +2 -9
  14. package/lib/service/candidate/SimilarityService.js +7 -35
  15. package/lib/service/chat/ChatAgent.js +28 -686
  16. package/lib/service/chat/ContextWindow.js +87 -3
  17. package/lib/service/chat/ConversationStore.js +3 -4
  18. package/lib/service/chat/ProjectSemanticMemory.js +9 -14
  19. package/lib/service/chat/ReasoningLayer.js +10 -54
  20. package/lib/service/chat/ToolRegistry.js +0 -52
  21. package/lib/service/chat/tools.js +7 -6
  22. package/lib/service/cursor/TokenBudget.js +4 -21
  23. package/lib/service/search/CrossEncoderReranker.js +163 -0
  24. package/lib/service/search/RetrievalFunnel.js +9 -36
  25. package/lib/service/skills/SignalCollector.js +28 -28
  26. package/lib/shared/similarity.js +101 -0
  27. package/lib/shared/token-utils.js +46 -0
  28. package/package.json +1 -1
  29. package/dashboard/dist/assets/index-9byoG7kd.js +0 -129
@@ -5,12 +5,12 @@
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-9byoG7kd.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-D8dCXLzr.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">
12
12
  <link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
13
- <link rel="modulepreload" crossorigin href="/assets/icons-C6kshpB1.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/icons-C7FN32VL.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-markdown-Dc1U8Kko.js">
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-BDmJqEkA.css">
16
16
  </head>
@@ -113,6 +113,36 @@ export class AiProvider {
113
113
  return { text, functionCalls: null };
114
114
  }
115
115
 
116
+ /**
117
+ * Structured Output — 请求 AI 返回严格 JSON 格式响应
118
+ *
119
+ * 子类覆盖以利用原生 JSON mode:
120
+ * - Gemini: responseMimeType: 'application/json' + responseSchema
121
+ * - OpenAI: response_format: { type: 'json_object' }
122
+ * - Claude: 无原生支持,使用默认实现 (chat + extractJSON)
123
+ *
124
+ * @param {string} prompt — 完整提示词(应包含返回 JSON 的指令)
125
+ * @param {object} [opts]
126
+ * @param {object} [opts.schema] — JSON Schema(Gemini/OpenAI 的 structured output 用)
127
+ * @param {string} [opts.openChar='{'] — extractJSON 边界起始符(fallback 用)
128
+ * @param {string} [opts.closeChar='}'] — extractJSON 边界终止符
129
+ * @param {number} [opts.temperature=0.3]
130
+ * @param {number} [opts.maxTokens=32768]
131
+ * @param {string} [opts.systemPrompt] — 可选系统指令
132
+ * @returns {Promise<any>} — 解析后的 JSON 对象/数组,解析失败返回 null
133
+ */
134
+ async chatWithStructuredOutput(prompt, opts = {}) {
135
+ const response = await this.chat(prompt, {
136
+ temperature: opts.temperature ?? 0.3,
137
+ maxTokens: opts.maxTokens ?? 32768,
138
+ systemPrompt: opts.systemPrompt,
139
+ });
140
+ if (!response || response.trim().length === 0) return null;
141
+ const openChar = opts.openChar || '{';
142
+ const closeChar = opts.closeChar || '}';
143
+ return this.extractJSON(response, openChar, closeChar);
144
+ }
145
+
116
146
  /**
117
147
  * 从源码文件批量提取 Recipe 结构(AI 驱动)
118
148
  * 默认实现使用 chat() + 标准提示词;子类可覆盖以使用专用 API
@@ -124,16 +154,14 @@ export class AiProvider {
124
154
  */
125
155
  async extractRecipes(targetName, filesContent, options = {}) {
126
156
  const prompt = this._buildExtractPrompt(targetName, filesContent, options);
127
- const response = await this.chat(prompt, { temperature: 0.3, maxTokens: 32768 });
128
- if (!response || response.trim().length === 0) {
129
- this._log('warn', `[extractRecipes] AI returned empty response for target: ${targetName}`);
130
- return [];
131
- }
132
- const parsed = this.extractJSON(response, '[', ']');
157
+ const parsed = await this.chatWithStructuredOutput(prompt, {
158
+ openChar: '[',
159
+ closeChar: ']',
160
+ temperature: 0.3,
161
+ maxTokens: 32768,
162
+ });
133
163
  if (!Array.isArray(parsed)) {
134
- // JSON 解析失败 记录原始响应尾部用于调试(截断诊断更有用)
135
- const tail = response.length > 300 ? response.substring(response.length - 300) : response;
136
- this._log('warn', `[extractRecipes] JSON parse failed for target: ${targetName}, response length: ${response.length}, tail: ${tail}`);
164
+ this._log('warn', `[extractRecipes] structured output parse failed for target: ${targetName}`);
137
165
  return [];
138
166
  }
139
167
  if (parsed.length === 0) {
@@ -454,8 +482,11 @@ ${files}`;
454
482
  */
455
483
  async enrichCandidates(candidates) {
456
484
  const prompt = this._buildEnrichPrompt(candidates);
457
- const response = await this.chat(prompt, { temperature: 0.3 });
458
- const parsed = this.extractJSON(response, '[', ']');
485
+ const parsed = await this.chatWithStructuredOutput(prompt, {
486
+ openChar: '[',
487
+ closeChar: ']',
488
+ temperature: 0.3,
489
+ });
459
490
  return Array.isArray(parsed) ? parsed : [];
460
491
  }
461
492
 
@@ -249,8 +249,10 @@ export class ClaudeProvider extends AiProvider {
249
249
 
250
250
  async summarize(code) {
251
251
  const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
252
- const text = await this.chat(prompt, { temperature: 0.3 });
253
- return this.extractJSON(text) || { title: '', description: text };
252
+ return await this.chatWithStructuredOutput(prompt, {
253
+ temperature: 0.3,
254
+ maxTokens: 4096,
255
+ }) || { title: '', description: '' };
254
256
  }
255
257
 
256
258
  async embed(_text) {
@@ -216,7 +216,8 @@ export class GoogleGeminiProvider extends AiProvider {
216
216
  }
217
217
 
218
218
  /**
219
- * 清理 JSON Schema 使之兼容 Gemini API 的 OpenAPI 子集
219
+ * 清理 JSON Schema 使之兼容 Gemini API 的 OpenAPI 子集(递归)
220
+ * Gemini API 不支持 default、examples 等 JSON Schema 扩展字段
220
221
  */
221
222
  #sanitizeSchemaForGemini(schema) {
222
223
  if (!schema || typeof schema !== 'object') {
@@ -224,20 +225,24 @@ export class GoogleGeminiProvider extends AiProvider {
224
225
  }
225
226
 
226
227
  const cleaned = { ...schema };
228
+ delete cleaned.default;
229
+ delete cleaned.examples;
227
230
  if (!cleaned.type) cleaned.type = 'object';
228
231
 
232
+ // 递归清理 properties
229
233
  if (cleaned.properties) {
230
234
  const props = {};
231
235
  for (const [key, val] of Object.entries(cleaned.properties)) {
232
- const prop = { ...val };
233
- delete prop.default;
234
- delete prop.examples;
235
- if (!prop.type) prop.type = 'string';
236
- props[key] = prop;
236
+ props[key] = this.#sanitizeSchemaForGemini(val);
237
237
  }
238
238
  cleaned.properties = props;
239
239
  }
240
240
 
241
+ // 递归清理 items (array 类型)
242
+ if (cleaned.items && typeof cleaned.items === 'object') {
243
+ cleaned.items = this.#sanitizeSchemaForGemini(cleaned.items);
244
+ }
245
+
241
246
  return cleaned;
242
247
  }
243
248
 
@@ -297,8 +302,61 @@ export class GoogleGeminiProvider extends AiProvider {
297
302
 
298
303
  async summarize(code) {
299
304
  const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
300
- const text = await this.chat(prompt, { temperature: 0.3 });
301
- return this.extractJSON(text) || { title: '', description: text };
305
+ return await this.chatWithStructuredOutput(prompt, {
306
+ temperature: 0.3,
307
+ maxTokens: 8192,
308
+ }) || { title: '', description: '' };
309
+ }
310
+
311
+ /**
312
+ * Structured Output — Gemini 原生 JSON mode
313
+ *
314
+ * 使用 responseMimeType: 'application/json' 强制 Gemini 返回合法 JSON。
315
+ * 可选传入 responseSchema 做编译期校验(Gemini 1.5+ / Gemini 2+)。
316
+ */
317
+ async chatWithStructuredOutput(prompt, opts = {}) {
318
+ return this._withRetry(async () => {
319
+ const {
320
+ schema,
321
+ temperature = 0.3,
322
+ maxTokens = 32768,
323
+ systemPrompt,
324
+ } = opts;
325
+
326
+ const contents = [{ role: 'user', parts: [{ text: prompt }] }];
327
+
328
+ const generationConfig = {
329
+ temperature,
330
+ maxOutputTokens: maxTokens,
331
+ responseMimeType: 'application/json',
332
+ };
333
+
334
+ // 如果提供了 JSON Schema,注入 responseSchema(Gemini 编译期校验)
335
+ if (schema) {
336
+ generationConfig.responseSchema = this.#sanitizeSchemaForGemini(schema);
337
+ }
338
+
339
+ const body = { contents, generationConfig };
340
+
341
+ if (systemPrompt) {
342
+ body.systemInstruction = { parts: [{ text: systemPrompt }] };
343
+ }
344
+
345
+ const url = `${GEMINI_BASE}/models/${this.model}:generateContent?key=${this.apiKey}`;
346
+ const data = await this._post(url, body);
347
+ const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
348
+
349
+ if (!text) return null;
350
+
351
+ try {
352
+ return JSON.parse(text);
353
+ } catch {
354
+ // Gemini JSON mode 偶尔返回前后有空白的 JSON,尝试 extractJSON 降级
355
+ const openChar = opts.openChar || '{';
356
+ const closeChar = opts.closeChar || '}';
357
+ return this.extractJSON(text, openChar, closeChar);
358
+ }
359
+ });
302
360
  }
303
361
 
304
362
  async embed(text) {
@@ -184,8 +184,54 @@ export class OpenAiProvider extends AiProvider {
184
184
 
185
185
  async summarize(code) {
186
186
  const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
187
- const text = await this.chat(prompt, { temperature: 0.3 });
188
- return this.extractJSON(text) || { title: '', description: text };
187
+ return await this.chatWithStructuredOutput(prompt, {
188
+ temperature: 0.3,
189
+ maxTokens: 4096,
190
+ }) || { title: '', description: '' };
191
+ }
192
+
193
+ /**
194
+ * Structured Output — OpenAI JSON mode
195
+ *
196
+ * 使用 response_format: { type: 'json_object' } 强制返回合法 JSON。
197
+ * 兼容 DeepSeek / Ollama 等 OpenAI-Compatible API。
198
+ */
199
+ async chatWithStructuredOutput(prompt, opts = {}) {
200
+ return this._withRetry(async () => {
201
+ const {
202
+ temperature = 0.3,
203
+ maxTokens = 32768,
204
+ systemPrompt,
205
+ } = opts;
206
+
207
+ const messages = [];
208
+ if (systemPrompt) {
209
+ messages.push({ role: 'system', content: systemPrompt });
210
+ }
211
+ messages.push({ role: 'user', content: prompt });
212
+
213
+ const body = {
214
+ model: this.model,
215
+ messages,
216
+ temperature,
217
+ max_tokens: maxTokens,
218
+ response_format: { type: 'json_object' },
219
+ };
220
+
221
+ const data = await this._post(`${this.baseUrl}/chat/completions`, body);
222
+ const text = data?.choices?.[0]?.message?.content || '';
223
+
224
+ if (!text) return null;
225
+
226
+ try {
227
+ return JSON.parse(text);
228
+ } catch {
229
+ // JSON mode 极少出错,降级到 extractJSON
230
+ const openChar = opts.openChar || '{';
231
+ const closeChar = opts.closeChar || '}';
232
+ return this.extractJSON(text, openChar, closeChar);
233
+ }
234
+ });
189
235
  }
190
236
 
191
237
  async embed(text) {
@@ -693,8 +693,7 @@ ${args.userPrompt ? `用户补充: ${args.userPrompt}` : ''}
693
693
  "insight": "架构洞察(一句话)"
694
694
  }`;
695
695
 
696
- const response = await aiProvider.chat(prompt, { temperature: 0.3 });
697
- const parsed = aiProvider.extractJSON(response, '{', '}');
696
+ const parsed = await aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 });
698
697
 
699
698
  if (!parsed) {
700
699
  errors.push({ id: entry.id, title: entry.title, error: 'AI returned no valid JSON' });
@@ -25,6 +25,7 @@ import spmRouter from './routes/spm.js';
25
25
  import violationsRouter from './routes/violations.js';
26
26
  import authRouter from './routes/auth.js';
27
27
  import skillsRouter from './routes/skills.js';
28
+ import candidatesRouter from './routes/candidates.js';
28
29
  import knowledgeRouter from './routes/knowledge.js';
29
30
  import recipesRouter from './routes/recipes.js';
30
31
  import wikiRouter from './routes/wiki.js';
@@ -257,6 +258,9 @@ export class HttpServer {
257
258
  // Skills 路由
258
259
  this.app.use(`${apiPrefix}/skills`, skillsRouter);
259
260
 
261
+ // Candidates 路由(AI 补齐/润色)
262
+ this.app.use(`${apiPrefix}/candidates`, candidatesRouter);
263
+
260
264
  // SPM 路由
261
265
  this.app.use(`${apiPrefix}/spm`, spmRouter);
262
266