autosnippet 3.3.9 → 3.4.1

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 (68) hide show
  1. package/README.md +44 -19
  2. package/config/default.json +1 -1
  3. package/dashboard/dist/assets/{index-DEU4tJtP.js → index-BX6r2fiy.js} +40 -40
  4. package/dashboard/dist/assets/index-BvZcGN02.css +1 -0
  5. package/dashboard/dist/index.html +2 -2
  6. package/dist/lib/agent/AgentRuntime.js +13 -1
  7. package/dist/lib/agent/AgentRuntimeTypes.d.ts +2 -0
  8. package/dist/lib/agent/PipelineStrategy.js +32 -2
  9. package/dist/lib/agent/context/ContextWindow.d.ts +2 -1
  10. package/dist/lib/agent/context/ContextWindow.js +18 -4
  11. package/dist/lib/agent/context/ExplorationTracker.js +6 -1
  12. package/dist/lib/agent/context/exploration/ExplorationStrategies.js +2 -1
  13. package/dist/lib/agent/core/LoopContext.d.ts +3 -0
  14. package/dist/lib/agent/core/LoopContext.js +3 -0
  15. package/dist/lib/agent/domain/EpisodicConsolidator.d.ts +5 -0
  16. package/dist/lib/agent/domain/EpisodicConsolidator.js +60 -5
  17. package/dist/lib/agent/domain/insight-analyst.d.ts +16 -0
  18. package/dist/lib/agent/domain/insight-analyst.js +38 -0
  19. package/dist/lib/agent/domain/insight-gate.js +12 -0
  20. package/dist/lib/agent/memory/MemoryConsolidator.js +17 -0
  21. package/dist/lib/core/AstAnalyzer.js +0 -1
  22. package/dist/lib/core/ast/lang-dart.js +0 -1
  23. package/dist/lib/core/ast/lang-go.js +0 -1
  24. package/dist/lib/core/ast/lang-java.js +0 -1
  25. package/dist/lib/core/ast/lang-javascript.js +0 -1
  26. package/dist/lib/core/ast/lang-objc.js +0 -1
  27. package/dist/lib/core/ast/lang-python.js +0 -1
  28. package/dist/lib/core/ast/lang-rust.js +0 -1
  29. package/dist/lib/core/ast/lang-swift.js +0 -1
  30. package/dist/lib/core/ast/lang-typescript.js +0 -1
  31. package/dist/lib/domain/dimension/DimensionRegistry.d.ts +6 -4
  32. package/dist/lib/domain/dimension/DimensionRegistry.js +19 -23
  33. package/dist/lib/external/ai/AiFactory.d.ts +14 -0
  34. package/dist/lib/external/ai/AiFactory.js +33 -1
  35. package/dist/lib/external/ai/AiProvider.d.ts +2 -0
  36. package/dist/lib/external/ai/AiProvider.js +4 -0
  37. package/dist/lib/external/ai/providers/ClaudeProvider.d.ts +1 -1
  38. package/dist/lib/external/ai/providers/ClaudeProvider.js +6 -2
  39. package/dist/lib/external/ai/providers/GoogleGeminiProvider.d.ts +1 -1
  40. package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +13 -5
  41. package/dist/lib/external/ai/providers/OpenAiProvider.d.ts +1 -1
  42. package/dist/lib/external/ai/providers/OpenAiProvider.js +8 -4
  43. package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +10 -2
  44. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
  45. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +39 -5
  46. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +2 -0
  47. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +4 -0
  48. package/dist/lib/external/mcp/handlers/guard.js +11 -6
  49. package/dist/lib/http/routes/ai.js +20 -3
  50. package/dist/lib/infrastructure/vector/IndexingPipeline.js +6 -1
  51. package/dist/lib/injection/modules/AiModule.js +22 -1
  52. package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +7 -0
  53. package/dist/lib/service/bootstrap/BootstrapTaskManager.js +17 -0
  54. package/dist/lib/service/guard/ComplianceReporter.js +5 -1
  55. package/dist/lib/service/guard/GuardCheckEngine.d.ts +12 -1
  56. package/dist/lib/service/guard/GuardCheckEngine.js +36 -4
  57. package/dist/lib/service/guard/GuardCodeChecks.js +27 -9
  58. package/dist/lib/service/guard/SourceFileCollector.d.ts +3 -2
  59. package/dist/lib/service/guard/SourceFileCollector.js +3 -3
  60. package/dist/lib/service/search/SearchEngine.js +165 -61
  61. package/dist/lib/service/task/PrimeSearchPipeline.js +17 -2
  62. package/dist/lib/service/vector/VectorService.js +10 -1
  63. package/dist/lib/shared/LanguageService.d.ts +12 -0
  64. package/dist/lib/shared/LanguageService.js +85 -0
  65. package/dist/lib/shared/schemas/http-requests.d.ts +4 -0
  66. package/dist/lib/shared/schemas/http-requests.js +4 -0
  67. package/package.json +1 -1
  68. package/dashboard/dist/assets/index-DHJ1Dj7u.css +0 -1
@@ -42,9 +42,11 @@ export function createProvider(options = {}) {
42
42
  break;
43
43
  case 'ollama':
44
44
  config.name = 'ollama';
45
- config.baseUrl = config.baseUrl || 'http://localhost:11434/v1';
45
+ config.baseUrl =
46
+ config.baseUrl || process.env.ASD_OLLAMA_BASE_URL || 'http://localhost:11434/v1';
46
47
  config.apiKey = config.apiKey || 'ollama';
47
48
  config.model = config.model || 'llama3';
49
+ config.embedModel = config.embedModel || 'qwen3-embedding:0.6b';
48
50
  break;
49
51
  default:
50
52
  break;
@@ -172,10 +174,37 @@ export async function getProviderWithFallback() {
172
174
  }
173
175
  return primary;
174
176
  }
177
+ /**
178
+ * 创建独立的 Embedding Provider
179
+ *
180
+ * 当 ASD_EMBED_PROVIDER 被设置时,创建一个专用于 embedding 的 provider 实例,
181
+ * 使 embedding 和 LLM 生成可以使用不同的提供商/模型。
182
+ *
183
+ * 典型场景:LLM 用 Google Gemini,Embedding 用本地 Ollama + qwen3-embedding
184
+ *
185
+ * @returns 独立的 embed provider,或 null(未配置时)
186
+ */
187
+ export function createEmbedProvider() {
188
+ const embedProviderName = process.env.ASD_EMBED_PROVIDER;
189
+ if (!embedProviderName) {
190
+ return null;
191
+ }
192
+ const logger = Logger.getInstance();
193
+ logger.info(`[AiFactory] Creating dedicated embed provider: ${embedProviderName}`);
194
+ return createProvider({
195
+ provider: embedProviderName,
196
+ model: process.env.ASD_EMBED_MODEL || undefined,
197
+ baseUrl: process.env.ASD_EMBED_BASE_URL || undefined,
198
+ apiKey: process.env.ASD_EMBED_API_KEY || undefined,
199
+ embedModel: process.env.ASD_EMBED_MODEL || undefined,
200
+ });
201
+ }
175
202
  /** 获取当前 AI 配置信息(同步,用于 UI 展示) */
176
203
  export function getAiConfigInfo() {
177
204
  const provider = process.env.ASD_AI_PROVIDER || 'auto';
178
205
  const model = process.env.ASD_AI_MODEL || '';
206
+ const embedProvider = process.env.ASD_EMBED_PROVIDER || '';
207
+ const embedModel = process.env.ASD_EMBED_MODEL || '';
179
208
  const hasGoogleKey = !!process.env.ASD_GOOGLE_API_KEY;
180
209
  const hasOpenAiKey = !!process.env.ASD_OPENAI_API_KEY;
181
210
  const hasClaudeKey = !!process.env.ASD_CLAUDE_API_KEY;
@@ -183,6 +212,8 @@ export function getAiConfigInfo() {
183
212
  return {
184
213
  provider,
185
214
  model,
215
+ embedProvider,
216
+ embedModel,
186
217
  hasKey: hasGoogleKey || hasOpenAiKey || hasClaudeKey || hasDeepSeekKey,
187
218
  keys: {
188
219
  google: hasGoogleKey,
@@ -200,6 +231,7 @@ export { MockProvider } from './providers/MockProvider.js';
200
231
  export { OpenAiProvider } from './providers/OpenAiProvider.js';
201
232
  export default {
202
233
  createProvider,
234
+ createEmbedProvider,
203
235
  autoDetectProvider,
204
236
  getAiConfigInfo,
205
237
  getProviderWithFallback,
@@ -57,6 +57,8 @@ export interface ChatWithToolsOptions {
57
57
  systemPrompt?: string;
58
58
  temperature?: number;
59
59
  maxTokens?: number;
60
+ /** 外部中止信号 — hard timeout 时取消进行中的 LLM 请求 */
61
+ abortSignal?: AbortSignal;
60
62
  }
61
63
  /** 函数调用结果 */
62
64
  export interface FunctionCallResult {
@@ -684,6 +684,10 @@ ${items}`;
684
684
  }
685
685
  catch (err) {
686
686
  const e = err;
687
+ // AbortError — 外部主动中止(如 PipelineStrategy hard timeout),不重试直接抛出
688
+ if (e.name === 'AbortError' || e.cause?.name === 'AbortError') {
689
+ throw e;
690
+ }
687
691
  // ── 综合判断是否为可重试的网络/服务端错误 ──
688
692
  const causeCode = e.cause?.code || '';
689
693
  // 网络级错误:无 HTTP status,底层连接失败
@@ -32,6 +32,6 @@ export declare class ClaudeProvider extends AiProvider {
32
32
  summarize(code: string): Promise<{}>;
33
33
  embed(_text: string | string[]): Promise<never[]>;
34
34
  supportsEmbedding(): boolean;
35
- _post(url: string, body: Record<string, unknown>): Promise<ApiResponse>;
35
+ _post(url: string, body: Record<string, unknown>, externalSignal?: AbortSignal): Promise<ApiResponse>;
36
36
  }
37
37
  export default ClaudeProvider;
@@ -100,7 +100,7 @@ export class ClaudeProvider extends AiProvider {
100
100
  body.tool_choice = { type: 'auto' };
101
101
  }
102
102
  }
103
- const data = await this._post(`${CLAUDE_BASE}/messages`, body);
103
+ const data = await this._post(`${CLAUDE_BASE}/messages`, body, opts.abortSignal);
104
104
  return this.#parseToolResponse(data);
105
105
  }
106
106
  // ─── 内部转换方法 ──────────────────────
@@ -241,7 +241,7 @@ export class ClaudeProvider extends AiProvider {
241
241
  supportsEmbedding() {
242
242
  return false;
243
243
  }
244
- async _post(url, body) {
244
+ async _post(url, body, externalSignal) {
245
245
  if (!this.apiKey) {
246
246
  const err = new Error('Claude API Key 未配置。请在 .env 中设置 ASD_CLAUDE_API_KEY,或运行 asd setup 完成配置。');
247
247
  err.code = 'API_KEY_MISSING';
@@ -249,6 +249,9 @@ export class ClaudeProvider extends AiProvider {
249
249
  }
250
250
  const controller = new AbortController();
251
251
  const timer = setTimeout(() => controller.abort(), this.timeout);
252
+ // 外部中止信号 → 联动本地 controller
253
+ const onExternalAbort = () => controller.abort();
254
+ externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
252
255
  try {
253
256
  const res = await this._fetch(url, {
254
257
  method: 'POST',
@@ -269,6 +272,7 @@ export class ClaudeProvider extends AiProvider {
269
272
  }
270
273
  finally {
271
274
  clearTimeout(timer);
275
+ externalSignal?.removeEventListener('abort', onExternalAbort);
272
276
  }
273
277
  }
274
278
  }
@@ -34,6 +34,6 @@ export declare class GoogleGeminiProvider extends AiProvider {
34
34
  */
35
35
  chatWithStructuredOutput(prompt: string, opts?: StructuredOutputOptions): Promise<any>;
36
36
  embed(text: string | string[]): Promise<number[] | number[][]>;
37
- _post(url: string, body: Record<string, unknown>): Promise<ApiResponse>;
37
+ _post(url: string, body: Record<string, unknown>, externalSignal?: AbortSignal): Promise<ApiResponse>;
38
38
  }
39
39
  export default GoogleGeminiProvider;
@@ -9,8 +9,9 @@
9
9
  import Logger from '#infra/logging/Logger.js';
10
10
  import { AiProvider, } from '../AiProvider.js';
11
11
  const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1beta';
12
- const EMBED_MODEL = 'models/gemini-embedding-001';
12
+ const DEFAULT_EMBED_MODEL = 'models/gemini-embedding-001';
13
13
  export class GoogleGeminiProvider extends AiProvider {
14
+ #embedModel;
14
15
  constructor(config = {}) {
15
16
  super({
16
17
  ...config,
@@ -20,6 +21,9 @@ export class GoogleGeminiProvider extends AiProvider {
20
21
  this.name = 'google-gemini';
21
22
  this.model = config.model || 'gemini-3-flash-preview';
22
23
  this.apiKey = config.apiKey || process.env.ASD_GOOGLE_API_KEY || '';
24
+ this.#embedModel = config.embedModel
25
+ ? `models/${config.embedModel.replace(/^models\//, '')}`
26
+ : DEFAULT_EMBED_MODEL;
23
27
  this.logger = Logger.getInstance();
24
28
  }
25
29
  /** 是否支持原生结构化函数调用 */
@@ -108,7 +112,7 @@ export class GoogleGeminiProvider extends AiProvider {
108
112
  body.systemInstruction = { parts: [{ text: systemPrompt }] };
109
113
  }
110
114
  const url = `${GEMINI_BASE}/models/${this.model}:generateContent?key=${this.apiKey}`;
111
- const data = await this._post(url, body);
115
+ const data = await this._post(url, body, opts.abortSignal);
112
116
  return this.#parseToolResponse(data);
113
117
  });
114
118
  }
@@ -345,10 +349,10 @@ export class GoogleGeminiProvider extends AiProvider {
345
349
  for (let i = 0; i < texts.length; i += 100) {
346
350
  const batch = texts.slice(i, i + 100);
347
351
  const requests = batch.map((t) => ({
348
- model: EMBED_MODEL,
352
+ model: this.#embedModel,
349
353
  content: { parts: [{ text: t.slice(0, 8000) }] },
350
354
  }));
351
- const url = `${GEMINI_BASE}/${EMBED_MODEL}:batchEmbedContents?key=${this.apiKey}`;
355
+ const url = `${GEMINI_BASE}/${this.#embedModel}:batchEmbedContents?key=${this.apiKey}`;
352
356
  const data = await this._post(url, { requests });
353
357
  if (data?.embeddings) {
354
358
  results.push(...data.embeddings.map((e) => e.values));
@@ -356,7 +360,7 @@ export class GoogleGeminiProvider extends AiProvider {
356
360
  }
357
361
  return Array.isArray(text) ? results : results[0] || [];
358
362
  }
359
- async _post(url, body) {
363
+ async _post(url, body, externalSignal) {
360
364
  if (!this.apiKey) {
361
365
  const err = new Error('Google Gemini API Key 未配置。请在 .env 中设置 ASD_GOOGLE_API_KEY,或运行 asd setup 完成配置。');
362
366
  err.code = 'API_KEY_MISSING';
@@ -364,6 +368,9 @@ export class GoogleGeminiProvider extends AiProvider {
364
368
  }
365
369
  const controller = new AbortController();
366
370
  const timer = setTimeout(() => controller.abort(), this.timeout);
371
+ // 外部中止信号 → 联动本地 controller
372
+ const onExternalAbort = () => controller.abort();
373
+ externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
367
374
  try {
368
375
  const res = await this._fetch(url, {
369
376
  method: 'POST',
@@ -401,6 +408,7 @@ export class GoogleGeminiProvider extends AiProvider {
401
408
  }
402
409
  finally {
403
410
  clearTimeout(timer);
411
+ externalSignal?.removeEventListener('abort', onExternalAbort);
404
412
  }
405
413
  }
406
414
  }
@@ -37,6 +37,6 @@ export declare class OpenAiProvider extends AiProvider {
37
37
  */
38
38
  chatWithStructuredOutput(prompt: string, opts?: StructuredOutputOptions): Promise<any>;
39
39
  embed(text: string | string[]): Promise<any>;
40
- _post(url: string, body: Record<string, unknown>): Promise<ApiResponse>;
40
+ _post(url: string, body: Record<string, unknown>, externalSignal?: AbortSignal): Promise<ApiResponse>;
41
41
  }
42
42
  export default OpenAiProvider;
@@ -14,10 +14,10 @@ export class OpenAiProvider extends AiProvider {
14
14
  constructor(config = {}) {
15
15
  super(config);
16
16
  this.name = config.name || 'openai';
17
- this.model = config.model || 'gpt-4o-mini';
17
+ this.model = config.model || 'gpt-5.4-mini';
18
18
  this.apiKey = config.apiKey || process.env.ASD_OPENAI_API_KEY || '';
19
19
  this.baseUrl = config.baseUrl || OPENAI_BASE;
20
- this.embedModel = config.embedModel || 'text-embedding-3-small';
20
+ this.embedModel = config.embedModel || process.env.ASD_EMBED_MODEL || 'text-embedding-3-small';
21
21
  this.logger = Logger.getInstance();
22
22
  }
23
23
  /**
@@ -129,7 +129,7 @@ export class OpenAiProvider extends AiProvider {
129
129
  else {
130
130
  body.tool_choice = 'auto';
131
131
  }
132
- const data = await this._post(`${this.baseUrl}/chat/completions`, body);
132
+ const data = await this._post(`${this.baseUrl}/chat/completions`, body, opts.abortSignal);
133
133
  return this.#parseToolResponse(data);
134
134
  });
135
135
  }
@@ -251,7 +251,7 @@ export class OpenAiProvider extends AiProvider {
251
251
  return Array.isArray(text) ? texts.map(() => []) : [];
252
252
  }
253
253
  }
254
- async _post(url, body) {
254
+ async _post(url, body, externalSignal) {
255
255
  // Ollama 使用固定 dummy key,不需要校验
256
256
  if (!this.apiKey && this.name !== 'ollama') {
257
257
  const envKey = this.name === 'deepseek' ? 'ASD_DEEPSEEK_API_KEY' : 'ASD_OPENAI_API_KEY';
@@ -261,6 +261,9 @@ export class OpenAiProvider extends AiProvider {
261
261
  }
262
262
  const controller = new AbortController();
263
263
  const timer = setTimeout(() => controller.abort(), this.timeout);
264
+ // 外部中止信号 → 联动本地 controller
265
+ const onExternalAbort = () => controller.abort();
266
+ externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
264
267
  try {
265
268
  const res = await this._fetch(url, {
266
269
  method: 'POST',
@@ -280,6 +283,7 @@ export class OpenAiProvider extends AiProvider {
280
283
  }
281
284
  finally {
282
285
  clearTimeout(timer);
286
+ externalSignal?.removeEventListener('abort', onExternalAbort);
283
287
  }
284
288
  }
285
289
  }
@@ -760,11 +760,19 @@ function buildExecutionPlan(activeDimensions) {
760
760
  const scheduler = new TierScheduler();
761
761
  const tiers = scheduler.getTiers();
762
762
  const activeDimIds = new Set(activeDimensions.map((d) => d.id));
763
- const tierLabels = ['基础数据层', '规范 + 架构 + 模式', '流转 + 实践 + 总结'];
763
+ const tierLabels = [
764
+ '基础数据层',
765
+ '规范 + 设计 + 网络',
766
+ '核心质量',
767
+ '领域专项',
768
+ '终端优化 + 总结',
769
+ ];
764
770
  const tierNotes = [
765
771
  '这些维度相互独立,可以任意顺序分析。产出的上下文将帮助后续维度。',
766
772
  '建议利用 Tier 1 中了解到的项目结构和代码特征。',
767
- 'agent-guidelines 应在最后分析 — 综合前序所有维度的发现。',
773
+ '利用前两层建立的架构和规范上下文深入分析。',
774
+ '各维度相对独立,可充分利用并行能力。',
775
+ 'agent-guidelines 应综合前序所有维度的发现。',
768
776
  ];
769
777
  const plan = tiers
770
778
  .map((tierDimIds, index) => {
@@ -68,6 +68,7 @@ interface BootstrapFileEntry {
68
68
  /** Task manager minimal shape */
69
69
  interface TaskManagerLike {
70
70
  isSessionValid(sessionId: string): boolean;
71
+ getSessionAbortSignal?(): AbortSignal | null;
71
72
  emitProgress?(event: string, data: Record<string, unknown>): void;
72
73
  [key: string]: unknown;
73
74
  }
@@ -18,7 +18,7 @@ import path from 'node:path';
18
18
  import { AgentMessage } from '#agent/AgentMessage.js';
19
19
  import { ExplorationTracker } from '#agent/context/ExplorationTracker.js';
20
20
  import { EpisodicConsolidator } from '#agent/domain/EpisodicConsolidator.js';
21
- import { ANALYST_BUDGET } from '#agent/domain/insight-analyst.js';
21
+ import { computeAnalystBudget } from '#agent/domain/insight-analyst.js';
22
22
  import { MemoryCoordinator } from '#agent/memory/MemoryCoordinator.js';
23
23
  import { MemoryEmbeddingStore } from '#agent/memory/MemoryEmbeddingStore.js';
24
24
  import { PersistentMemory } from '#agent/memory/PersistentMemory.js';
@@ -113,6 +113,7 @@ export async function fillDimensionsV3(view, dimensions) {
113
113
  /* not available */
114
114
  }
115
115
  const sessionId = view.bootstrapSession?.id ?? '';
116
+ const sessionAbortSignal = taskManager?.getSessionAbortSignal?.() ?? null;
116
117
  const isIncremental = incrementalPlan?.canIncremental && incrementalPlan?.mode === 'incremental';
117
118
  const emitter = new BootstrapEventEmitter(ctx.container);
118
119
  logger.info(`[Insight-v3] ═══ fillDimensionsV3 entered — ${isIncremental ? 'INCREMENTAL' : 'FULL'} pipeline`);
@@ -600,9 +601,9 @@ export async function fillDimensionsV3(view, dimensions) {
600
601
  // ── 引擎增强参数 (PipelineStrategy → reactLoop 透传) ──
601
602
  contextWindow: agentFactory?.createContextWindow({ isSystem: true }),
602
603
  // B1 fix: 分析阶段使用 analyst 策略 (SCAN→EXPLORE→VERIFY→SUMMARIZE)
603
- // 而非 bootstrap (EXPLORE→PRODUCE→SUMMARIZE),避免 PRODUCE nudge 浪费轮次
604
- // B3 fix: 透传完整 ANALYST_BUDGET (searchBudget/maxSubmits/softSubmitLimit/idleRoundsToExit)
605
- tracker: ExplorationTracker.resolve({ source: 'system', strategy: 'analyst' }, { ...ANALYST_BUDGET }),
604
+ // B3 fix: 透传完整 ANALYST_BUDGET
605
+ // B4 fix: 自适应预算 根据项目文件数缩放 maxIterations (24~40)
606
+ tracker: ExplorationTracker.resolve({ source: 'system', strategy: 'analyst' }, computeAnalystBudget(projectInfo.fileCount || 0)),
606
607
  trace: memoryCoordinator.getActiveContext(analystScopeId),
607
608
  memoryCoordinator,
608
609
  sharedState: {
@@ -623,7 +624,7 @@ export async function fillDimensionsV3(view, dimensions) {
623
624
  // 外层超时 = 安全网 (各阶段已有独立超时: Analyst 300s + Producer 180s + 硬缓冲 60s)
624
625
  const outerTimeoutMs = 3_600_000; // 1 小时——维度分析本身耗时长
625
626
  const runResult = await Promise.race([
626
- runtime.execute(message, { strategyContext }),
627
+ runtime.execute(message, { strategyContext, abortSignal: sessionAbortSignal }),
627
628
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Bootstrap runtime timeout for "${dimId}"`)), outerTimeoutMs)),
628
629
  ]);
629
630
  // ── 提取结果 ──
@@ -826,6 +827,7 @@ export async function fillDimensionsV3(view, dimensions) {
826
827
  source: 'enhanced-pipeline-strategy',
827
828
  });
828
829
  const dimTokenUsage = combinedTokenUsage;
830
+ const qualityScores = artifact.qualityReport;
829
831
  const dimResult = {
830
832
  candidateCount: producerResult.candidateCount,
831
833
  rejectedCount: producerResult.rejectedCount || 0,
@@ -836,6 +838,13 @@ export async function fillDimensionsV3(view, dimensions) {
836
838
  tokenUsage: dimTokenUsage,
837
839
  analysisText: analysisReport.analysisText,
838
840
  referencedFilesList: analysisReport.referencedFiles || [],
841
+ qualityGate: qualityScores
842
+ ? {
843
+ totalScore: qualityScores.totalScore,
844
+ scores: qualityScores.scores,
845
+ action: gateResult?.action || (runResult?.degraded ? 'degrade' : 'pass'),
846
+ }
847
+ : null,
839
848
  };
840
849
  dimensionStats[dimId] = dimResult;
841
850
  // P3: 保存 checkpoint — 仅当有实质分析内容时(避免 degraded/空结果污染后续 run)
@@ -1061,6 +1070,30 @@ export async function fillDimensionsV3(view, dimensions) {
1061
1070
  `~${consolidationResult.total.updated} UPDATE, ` +
1062
1071
  `⊕${consolidationResult.total.merged} MERGE | ` +
1063
1072
  `Total: ${smStats.total} memories (avg importance: ${smStats.avgImportance})`);
1073
+ // 按类型和来源分布
1074
+ logger.info(`[Insight-v3] Memory by type: ${Object.entries(smStats.byType)
1075
+ .map(([t, n]) => `${t}=${n}`)
1076
+ .join(', ')} | ` +
1077
+ `by source: ${Object.entries(smStats.bySource)
1078
+ .map(([s, n]) => `${s}=${n}`)
1079
+ .join(', ')}`);
1080
+ // 蒸馏管线详情 (来自 EpisodicConsolidator 增强返回值)
1081
+ const cr = consolidationResult;
1082
+ if (cr.perDimension) {
1083
+ const perDim = cr.perDimension;
1084
+ const topDims = Object.entries(perDim)
1085
+ .sort(([, a], [, b]) => b - a)
1086
+ .slice(0, 6);
1087
+ logger.info(`[Insight-v3] Memory per-dimension (top ${topDims.length}): ${topDims.map(([d, n]) => `${d}=${n}`).join(', ')}`);
1088
+ }
1089
+ if (cr.importanceDistribution) {
1090
+ const hist = cr.importanceDistribution;
1091
+ const histStr = Object.entries(hist)
1092
+ .sort(([a], [b]) => Number(a) - Number(b))
1093
+ .map(([k, v]) => `imp${k}=${v}`)
1094
+ .join(' ');
1095
+ logger.info(`[Insight-v3] Importance histogram: ${histStr}`);
1096
+ }
1064
1097
  }
1065
1098
  else {
1066
1099
  logger.warn('[Insight-v3] Database not available — skipping Semantic Memory consolidation');
@@ -1163,6 +1196,7 @@ export async function fillDimensionsV3(view, dimensions) {
1163
1196
  durationMs: stat.durationMs || 0,
1164
1197
  toolCallCount: stat.toolCallCount || 0,
1165
1198
  tokenUsage: stat.tokenUsage || { input: 0, output: 0 },
1199
+ qualityGate: stat.qualityGate || null,
1166
1200
  };
1167
1201
  }
1168
1202
  // Phase E: 附加 Code Entity Graph 拓扑到报告
@@ -39,6 +39,8 @@ interface BootstrapFileEntry {
39
39
  relativePath: string;
40
40
  content: string;
41
41
  targetName: string;
42
+ /** Whether this file belongs to a test target or matches test file naming patterns */
43
+ isTest: boolean;
42
44
  }
43
45
  /** Target item — either a plain string or an object with metadata */
44
46
  type TargetItem = string | {
@@ -67,6 +67,7 @@ export async function runPhase1_FileCollection(projectRoot, logger, options = {}
67
67
  const seenPaths = new Set();
68
68
  const allFiles = [];
69
69
  for (const t of allTargets) {
70
+ const isTestTarget = typeof t === 'object' && /^test/i.test(t.type || '');
70
71
  try {
71
72
  const fileList = await discoverer.getTargetFiles(t);
72
73
  for (const f of fileList) {
@@ -86,6 +87,7 @@ export async function runPhase1_FileCollection(projectRoot, logger, options = {}
86
87
  relativePath: f.relativePath || path.basename(fp),
87
88
  content,
88
89
  targetName: typeof t === 'string' ? t : t.name,
90
+ isTest: isTestTarget || LanguageService.isTestFile(fp),
89
91
  });
90
92
  }
91
93
  catch {
@@ -382,6 +384,7 @@ export async function runPhase3_GuardAudit(allFiles, container, logger, options
382
384
  const guardFiles = allFiles.map((f) => ({
383
385
  path: f.path,
384
386
  content: f.content,
387
+ isTest: f.isTest,
385
388
  }));
386
389
  guardAudit = guardEngine.auditFiles(guardFiles, { scope: 'project' });
387
390
  // 写入 ViolationsStore
@@ -484,6 +487,7 @@ export async function runPhase4_DimensionResolve(params) {
484
487
  const guardFiles = allFiles.map((f) => ({
485
488
  path: f.path,
486
489
  content: f.content,
490
+ isTest: f.isTest,
487
491
  }));
488
492
  guardAudit = guardEngine.auditFiles(guardFiles, { scope: 'project' });
489
493
  logger.info(`[Bootstrap] Guard re-audit with ${guardEngine.getExternalRuleCount()} Enhancement Pack rules → ${guardAudit.summary?.totalViolations ?? 0} total violations`);
@@ -10,6 +10,7 @@ import { execSync } from 'node:child_process';
10
10
  import fs from 'node:fs';
11
11
  import { readFile } from 'node:fs/promises';
12
12
  import path from 'node:path';
13
+ import { LanguageService } from '#shared/LanguageService.js';
13
14
  import { resolveProjectRoot } from '#shared/resolveProjectRoot.js';
14
15
  import { envelope } from '../envelope.js';
15
16
  // ═══ Review 轮次追踪(模块私有) ═══════════════════
@@ -93,7 +94,7 @@ export async function guardAuditFiles(ctx, args) {
93
94
  content = '';
94
95
  }
95
96
  }
96
- return { path: absPath, content };
97
+ return { path: absPath, content, isTest: LanguageService.isTestFile(absPath) };
97
98
  }));
98
99
  const result = engine.auditFiles(filesToAudit, { scope });
99
100
  // 写入 ViolationsStore + GuardFeedbackLoop
@@ -224,7 +225,7 @@ export async function guardReview(ctx, args) {
224
225
  for (const fp of filePaths) {
225
226
  try {
226
227
  const code = await readFile(fp, 'utf8');
227
- const auditResult = engine.auditFile(fp, code);
228
+ const auditResult = engine.auditFile(fp, code, { isTest: LanguageService.isTestFile(fp) });
228
229
  const violations = auditResult.violations;
229
230
  // 收集 uncertain
230
231
  if (auditResult.uncertainResults?.length) {
@@ -523,7 +524,7 @@ export async function scanProject(ctx, args) {
523
524
  content = '';
524
525
  }
525
526
  }
526
- return { path: f.path, content };
527
+ return { path: f.path, content, isTest: LanguageService.isTestFile(f.path) };
527
528
  }));
528
529
  guardAudit = engine.auditFiles(filesToAudit, { scope: 'project' });
529
530
  // 写入 ViolationsStore
@@ -629,9 +630,13 @@ async function _injectEnhancementGuardRules(engine, ctx) {
629
630
  try {
630
631
  const { initEnhancementRegistry } = await import('#core/enhancement/index.js');
631
632
  const enhReg = await initEnhancementRegistry();
632
- // 使用空语言+空框架列表获取所有已注册的 Pack(不过滤)
633
- // 这里我们注入 ALL 规则,让 GuardCheckEngine languages 字段自行过滤
634
- const allPacks = enhReg.all();
633
+ // 仅注入无框架条件的通用 Pack 规则(如 go-web 无 frameworks 条件)
634
+ // 有框架条件的 Pack(如 go-grpc 需要 frameworks: ['grpc'])由 Bootstrap Phase 4
635
+ // 通过 resolve(lang, detectedFrameworks) 精确注入,避免非 gRPC 项目出现误报
636
+ const allPacks = enhReg.all().filter((pack) => {
637
+ const cond = pack.conditions;
638
+ return !cond?.frameworks?.length;
639
+ });
635
640
  const allGuardRules = [];
636
641
  for (const pack of allPacks) {
637
642
  try {
@@ -70,9 +70,9 @@ router.get('/providers', async (req, res) => {
70
70
  };
71
71
  const providers = [
72
72
  { id: 'google', label: 'Google Gemini', defaultModel: 'gemini-3-flash-preview' },
73
- { id: 'openai', label: 'OpenAI', defaultModel: 'gpt-4o' },
73
+ { id: 'openai', label: 'OpenAI', defaultModel: 'gpt-5.4' },
74
74
  { id: 'deepseek', label: 'DeepSeek', defaultModel: 'deepseek-chat' },
75
- { id: 'claude', label: 'Claude', defaultModel: 'claude-3-5-sonnet-20240620' },
75
+ { id: 'claude', label: 'Claude', defaultModel: 'claude-sonnet-4-20250514' },
76
76
  { id: 'ollama', label: 'Ollama', defaultModel: 'llama3' },
77
77
  { id: 'mock', label: 'Mock (测试)', defaultModel: 'mock-l3' },
78
78
  ].map((p) => ({
@@ -462,6 +462,10 @@ const LLM_ENV_KEYS = [
462
462
  'ASD_CLAUDE_API_KEY',
463
463
  'ASD_DEEPSEEK_API_KEY',
464
464
  'ASD_AI_PROXY',
465
+ 'ASD_EMBED_PROVIDER',
466
+ 'ASD_EMBED_MODEL',
467
+ 'ASD_EMBED_BASE_URL',
468
+ 'ASD_EMBED_API_KEY',
465
469
  ];
466
470
  /**
467
471
  * 解析 .env 内容为 key-value(仅提取 LLM 相关变量)
@@ -523,7 +527,7 @@ router.get('/env-config', async (req, res) => {
523
527
  * Body: { provider, model, apiKey, proxy? }
524
528
  */
525
529
  router.post('/env-config', validate(AiEnvConfigBody), async (req, res) => {
526
- const { provider, model, apiKey, proxy } = req.body;
530
+ const { provider, model, apiKey, proxy, embedProvider, embedModel, embedBaseUrl, embedApiKey } = req.body;
527
531
  const envPath = _getProjectEnvPath();
528
532
  let content = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
529
533
  // 构建 key-value 更新列表
@@ -547,6 +551,19 @@ router.post('/env-config', validate(AiEnvConfigBody), async (req, res) => {
547
551
  if (keyName && apiKey) {
548
552
  updates[keyName] = apiKey;
549
553
  }
554
+ // Embedding 独立配置
555
+ if (embedProvider) {
556
+ updates.ASD_EMBED_PROVIDER = embedProvider;
557
+ if (embedModel) {
558
+ updates.ASD_EMBED_MODEL = embedModel;
559
+ }
560
+ if (embedBaseUrl) {
561
+ updates.ASD_EMBED_BASE_URL = embedBaseUrl;
562
+ }
563
+ if (embedApiKey) {
564
+ updates.ASD_EMBED_API_KEY = embedApiKey;
565
+ }
566
+ }
550
567
  // 逐条合并到 .env 内容
551
568
  for (const [k, v] of Object.entries(updates)) {
552
569
  // 匹配已有行(包括被注释的行)
@@ -44,7 +44,12 @@ export class IndexingPipeline {
44
44
  constructor(options = {}) {
45
45
  this.#vectorStore = options.vectorStore || null;
46
46
  this.#aiProvider = options.aiProvider || null;
47
- this.#scanDirs = options.scanDirs || ['recipes', `${KNOWLEDGE_BASE_DIR}/recipes`];
47
+ this.#scanDirs = options.scanDirs || [
48
+ 'recipes',
49
+ 'candidates',
50
+ `${KNOWLEDGE_BASE_DIR}/recipes`,
51
+ `${KNOWLEDGE_BASE_DIR}/candidates`,
52
+ ];
48
53
  this.#projectRoot = options.projectRoot || process.cwd();
49
54
  this.#chunkingOptions = {
50
55
  strategy: options.chunking?.strategy ?? 'auto',
@@ -72,7 +72,28 @@ export async function initialize(c) {
72
72
  // Token 追踪 AOP(manager 自身已在构造时 wire,此处延迟注入 recorder)
73
73
  // recorder 注入放到 register() 之后(tokenUsageStore 需先注册)
74
74
  // Embedding fallback: manager 的 embedFallbackInit 回调已绑定,初始化时主动触发一次
75
- const initialEmbed = createEmbedFallback(c, c.singletons.aiProvider);
75
+ // 优先使用独立的 embed provider(ASD_EMBED_PROVIDER),其次 fallback 机制
76
+ let initialEmbed = null;
77
+ try {
78
+ const aiFactory = c.singletons._aiFactory;
79
+ if (typeof aiFactory?.createEmbedProvider === 'function') {
80
+ initialEmbed = aiFactory.createEmbedProvider();
81
+ if (initialEmbed) {
82
+ logger.info('Dedicated embed provider created from ASD_EMBED_PROVIDER', {
83
+ provider: initialEmbed.name,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ catch (err) {
89
+ logger.warn('Failed to create dedicated embed provider', {
90
+ error: err.message,
91
+ });
92
+ }
93
+ // 若无独立 embed provider,走旧的 fallback 逻辑
94
+ if (!initialEmbed) {
95
+ initialEmbed = createEmbedFallback(c, c.singletons.aiProvider);
96
+ }
76
97
  if (initialEmbed) {
77
98
  manager.setEmbedProvider(initialEmbed);
78
99
  c.singletons._embedProvider = initialEmbed;
@@ -116,6 +116,13 @@ export declare class BootstrapTaskManager {
116
116
  * @param [reason='Aborted by user']
117
117
  */
118
118
  abortSession(reason?: string): void;
119
+ /**
120
+ * 获取当前 session 的 AbortSignal
121
+ *
122
+ * 用于传入 AgentRuntime.execute(),使得 abortSession() 可以立即中断正在执行的 AI 调用,
123
+ * 而不是等到下一个维度边界才检测到取消。
124
+ */
125
+ getSessionAbortSignal(): AbortSignal | null;
119
126
  /**
120
127
  * 验证 sessionId 是否仍然是活跃 session
121
128
  *
@@ -119,6 +119,8 @@ export class BootstrapTaskManager {
119
119
  #currentSession = null;
120
120
  #eventBus = null;
121
121
  #signalBus = null;
122
+ /** 当前 session 的 AbortController,用于取消正在执行的 AI 调用 */
123
+ #sessionAbortController = null;
122
124
  /** 获取 RealtimeService 的 getter(延迟获取,避免循环依赖) */
123
125
  #getRealtimeService = null;
124
126
  constructor({ eventBus, signalBus, getRealtimeService } = {}) {
@@ -144,6 +146,7 @@ export class BootstrapTaskManager {
144
146
  }
145
147
  const sessionId = `bs_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
146
148
  this.#currentSession = new BootstrapSession(sessionId);
149
+ this.#sessionAbortController = new AbortController();
147
150
  for (const { id, meta } of taskDefs) {
148
151
  this.#currentSession.addTask(id, meta);
149
152
  }
@@ -177,6 +180,11 @@ export class BootstrapTaskManager {
177
180
  task.error = reason;
178
181
  }
179
182
  }
183
+ // 触发 AbortController,中断正在执行的 AI 调用
184
+ if (this.#sessionAbortController) {
185
+ this.#sessionAbortController.abort(reason);
186
+ this.#sessionAbortController = null;
187
+ }
180
188
  session.status = 'aborted';
181
189
  session.completedAt = Date.now();
182
190
  session.summary = {
@@ -209,6 +217,15 @@ export class BootstrapTaskManager {
209
217
  },
210
218
  });
211
219
  }
220
+ /**
221
+ * 获取当前 session 的 AbortSignal
222
+ *
223
+ * 用于传入 AgentRuntime.execute(),使得 abortSession() 可以立即中断正在执行的 AI 调用,
224
+ * 而不是等到下一个维度边界才检测到取消。
225
+ */
226
+ getSessionAbortSignal() {
227
+ return this.#sessionAbortController?.signal ?? null;
228
+ }
212
229
  /**
213
230
  * 验证 sessionId 是否仍然是活跃 session
214
231
  *