autosnippet 2.6.0 → 2.7.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 (66) hide show
  1. package/bin/cli.js +1 -1
  2. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-Cq4-iQhP.js} +148 -88
  3. package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
  4. package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -0
  5. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  6. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  7. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  8. package/dashboard/dist/index.html +6 -6
  9. package/lib/bootstrap.js +1 -1
  10. package/lib/cli/SetupService.js +33 -8
  11. package/lib/cli/UpgradeService.js +139 -2
  12. package/lib/core/ast/ProjectGraph.js +599 -0
  13. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  14. package/lib/domain/recipe/Recipe.js +3 -0
  15. package/lib/external/ai/AiProvider.js +83 -20
  16. package/lib/external/ai/providers/ClaudeProvider.js +197 -0
  17. package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
  18. package/lib/external/ai/providers/OpenAiProvider.js +131 -0
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  20. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
  21. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
  22. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  23. package/lib/external/mcp/handlers/bootstrap.js +151 -1634
  24. package/lib/external/mcp/handlers/browse.js +1 -1
  25. package/lib/external/mcp/handlers/candidate.js +1 -33
  26. package/lib/external/mcp/handlers/skill.js +54 -17
  27. package/lib/external/mcp/tools.js +4 -3
  28. package/lib/http/middleware/requestLogger.js +23 -4
  29. package/lib/http/routes/ai.js +3 -1
  30. package/lib/http/routes/auth.js +3 -2
  31. package/lib/http/routes/candidates.js +49 -25
  32. package/lib/http/routes/commands.js +0 -8
  33. package/lib/http/routes/guardRules.js +1 -16
  34. package/lib/http/routes/recipes.js +4 -17
  35. package/lib/http/routes/search.js +11 -19
  36. package/lib/http/routes/skills.js +2 -0
  37. package/lib/http/routes/snippets.js +0 -33
  38. package/lib/http/routes/spm.js +37 -63
  39. package/lib/http/utils/routeHelpers.js +31 -0
  40. package/lib/infrastructure/config/Paths.js +9 -0
  41. package/lib/infrastructure/logging/Logger.js +86 -3
  42. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  43. package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
  44. package/lib/injection/ServiceContainer.js +55 -2
  45. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  46. package/lib/service/candidate/CandidateFileWriter.js +68 -27
  47. package/lib/service/candidate/CandidateService.js +156 -10
  48. package/lib/service/chat/AnalystAgent.js +216 -0
  49. package/lib/service/chat/CandidateGuardrail.js +134 -0
  50. package/lib/service/chat/ChatAgent.js +1036 -167
  51. package/lib/service/chat/ContextWindow.js +730 -0
  52. package/lib/service/chat/HandoffProtocol.js +180 -0
  53. package/lib/service/chat/ProducerAgent.js +240 -0
  54. package/lib/service/chat/ToolRegistry.js +149 -5
  55. package/lib/service/chat/tools.js +1397 -61
  56. package/lib/service/recipe/RecipeFileWriter.js +12 -1
  57. package/lib/service/skills/SignalCollector.js +31 -6
  58. package/lib/service/skills/SkillAdvisor.js +2 -1
  59. package/lib/service/skills/SkillHooks.js +13 -5
  60. package/lib/service/spm/SpmService.js +2 -2
  61. package/package.json +1 -1
  62. package/templates/copilot-instructions.md +20 -3
  63. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  64. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  65. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  66. package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * OpenAiProvider - OpenAI / DeepSeek / Ollama 兼容提供商
3
3
  * 使用标准 OpenAI Chat Completions API
4
+ *
5
+ * v2: 支持原生 Function Calling(结构化工具调用)
6
+ * - 使用 Chat Completions API 的 tools + tool_choice 参数
7
+ * - 兼容 DeepSeek / Ollama 等 OpenAI-compatible API
4
8
  */
5
9
 
6
10
  import { AiProvider } from '../AiProvider.js';
@@ -19,6 +23,14 @@ export class OpenAiProvider extends AiProvider {
19
23
  this.logger = Logger.getInstance();
20
24
  }
21
25
 
26
+ /**
27
+ * 是否支持原生结构化函数调用
28
+ * OpenAI / DeepSeek Chat Completions API 均支持
29
+ */
30
+ get supportsNativeToolCalling() {
31
+ return true;
32
+ }
33
+
22
34
  async chat(prompt, context = {}) {
23
35
  return this._withRetry(async () => {
24
36
  const { history = [], temperature = 0.7, maxTokens = 4096 } = context;
@@ -41,6 +53,125 @@ export class OpenAiProvider extends AiProvider {
41
53
  });
42
54
  }
43
55
 
56
+ /**
57
+ * 带工具声明的结构化对话 — OpenAI Chat Completions Function Calling
58
+ *
59
+ * 接受统一消息格式,内部转换为 OpenAI Chat Completions 消息格式。
60
+ * 兼容 DeepSeek / Ollama 等 OpenAI-Compatible API。
61
+ *
62
+ * @param {string} prompt — fallback prompt
63
+ * @param {object} opts — 统一参数
64
+ * @returns {Promise<{text: string|null, functionCalls: Array<{id, name, args}>|null}>}
65
+ */
66
+ async chatWithTools(prompt, opts = {}) {
67
+ return this._withRetry(async () => {
68
+ const {
69
+ messages: unifiedMessages,
70
+ toolSchemas,
71
+ toolChoice = 'auto',
72
+ systemPrompt,
73
+ temperature = 0.7,
74
+ maxTokens = 4096,
75
+ } = opts;
76
+
77
+ // 统一消息 → OpenAI Chat Completions messages
78
+ const messages = [];
79
+ if (systemPrompt) {
80
+ messages.push({ role: 'system', content: systemPrompt });
81
+ }
82
+
83
+ const srcMessages = unifiedMessages?.length > 0
84
+ ? unifiedMessages
85
+ : [{ role: 'user', content: prompt }];
86
+
87
+ for (const msg of srcMessages) {
88
+ if (msg.role === 'user') {
89
+ messages.push({ role: 'user', content: msg.content });
90
+ } else if (msg.role === 'assistant') {
91
+ const m = { role: 'assistant', content: msg.content || null };
92
+ if (msg.toolCalls?.length > 0) {
93
+ m.tool_calls = msg.toolCalls.map(tc => ({
94
+ id: tc.id,
95
+ type: 'function',
96
+ function: {
97
+ name: tc.name,
98
+ arguments: JSON.stringify(tc.args || {}),
99
+ },
100
+ }));
101
+ }
102
+ messages.push(m);
103
+ } else if (msg.role === 'tool') {
104
+ messages.push({
105
+ role: 'tool',
106
+ tool_call_id: msg.toolCallId,
107
+ content: msg.content || '',
108
+ });
109
+ }
110
+ }
111
+
112
+ const body = {
113
+ model: this.model,
114
+ messages,
115
+ temperature,
116
+ max_tokens: maxTokens,
117
+ };
118
+
119
+ // 标准 tool schemas → OpenAI tools format
120
+ if (toolSchemas?.length > 0) {
121
+ body.tools = toolSchemas.map(s => ({
122
+ type: 'function',
123
+ function: {
124
+ name: s.name,
125
+ description: s.description || '',
126
+ parameters: s.parameters || { type: 'object', properties: {} },
127
+ },
128
+ }));
129
+ }
130
+
131
+ // toolChoice → OpenAI tool_choice
132
+ if (toolChoice === 'required') body.tool_choice = 'required';
133
+ else if (toolChoice === 'none') body.tool_choice = 'none';
134
+ else body.tool_choice = 'auto';
135
+
136
+ const data = await this._post(`${this.baseUrl}/chat/completions`, body);
137
+ return this.#parseToolResponse(data);
138
+ });
139
+ }
140
+
141
+ /**
142
+ * 解析 OpenAI Chat Completions 响应 — 提取 tool_calls 或 text
143
+ *
144
+ * OpenAI 返回格式:
145
+ * choices[0].message.tool_calls[]: { id, type: 'function', function: { name, arguments(JSON str) } }
146
+ */
147
+ #parseToolResponse(data) {
148
+ const choice = data?.choices?.[0];
149
+ if (!choice) return { text: '', functionCalls: null };
150
+
151
+ const message = choice.message;
152
+ const text = message?.content || null;
153
+
154
+ if (message?.tool_calls?.length > 0) {
155
+ const functionCalls = message.tool_calls
156
+ .filter(tc => tc.type === 'function')
157
+ .map(tc => ({
158
+ id: tc.id,
159
+ name: tc.function.name,
160
+ args: (() => {
161
+ try { return JSON.parse(tc.function.arguments || '{}'); }
162
+ catch { return {}; }
163
+ })(),
164
+ }));
165
+
166
+ if (functionCalls.length > 0) {
167
+ this.logger.debug(`[OpenAI] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
168
+ return { text, functionCalls };
169
+ }
170
+ }
171
+
172
+ return { text, functionCalls: null };
173
+ }
174
+
44
175
  async summarize(code) {
45
176
  const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
46
177
  const text = await this.chat(prompt, { temperature: 0.3 });
@@ -0,0 +1,216 @@
1
+ /**
2
+ * pipeline/dimension-context.js — 跨维度上下文管理 (v6)
3
+ *
4
+ * 在 ChatAgent 按维度分批生产候选时,维护跨维度的上下文:
5
+ * - 项目基础信息 (不变)
6
+ * - 已完成维度的 DimensionDigest (累积)
7
+ * - 已提交候选的摘要列表 (累积)
8
+ *
9
+ * 确保每个维度的 ChatAgent 都能看到前序维度的分析结论,
10
+ * 实现跨维度透明互补。
11
+ *
12
+ * @module pipeline/dimension-context
13
+ */
14
+
15
+ /**
16
+ * DimensionContext — 跨维度上下文容器
17
+ *
18
+ * 在单次 bootstrap 会话中创建一次,按维度累积上下文。
19
+ */
20
+ export class DimensionContext {
21
+ /**
22
+ * @param {object} projectContext — 项目基础信息 (全程不变)
23
+ * @param {string} projectContext.projectName
24
+ * @param {string} projectContext.primaryLang
25
+ * @param {number} projectContext.fileCount
26
+ * @param {number} projectContext.targetCount
27
+ * @param {string[]} projectContext.modules
28
+ * @param {object} [projectContext.depGraph]
29
+ * @param {object} [projectContext.astMetrics]
30
+ * @param {object} [projectContext.guardSummary]
31
+ */
32
+ constructor(projectContext) {
33
+ /** @type {object} 项目基础信息 */
34
+ this.projectContext = projectContext;
35
+
36
+ /** @type {Map<string, DimensionDigest>} 已完成维度的摘要 */
37
+ this.completedDimensions = new Map();
38
+
39
+ /** @type {Array<CandidateSummary>} 已提交候选的摘要 */
40
+ this.submittedCandidates = [];
41
+ }
42
+
43
+ /**
44
+ * 维度完成后存储其摘要
45
+ *
46
+ * @param {string} dimId — 维度 ID
47
+ * @param {DimensionDigest} digest — 维度分析摘要
48
+ */
49
+ addDimensionDigest(dimId, digest) {
50
+ this.completedDimensions.set(dimId, {
51
+ ...digest,
52
+ dimId,
53
+ completedAt: Date.now(),
54
+ });
55
+ }
56
+
57
+ /**
58
+ * 记录已提交的候选
59
+ *
60
+ * @param {string} dimId
61
+ * @param {object} candidateInfo — { title, subTopic, summary }
62
+ */
63
+ addSubmittedCandidate(dimId, candidateInfo) {
64
+ this.submittedCandidates.push({
65
+ dimId,
66
+ title: candidateInfo.title || '',
67
+ subTopic: candidateInfo.subTopic || '',
68
+ summary: candidateInfo.summary || '',
69
+ });
70
+ }
71
+
72
+ /**
73
+ * 构建给 ChatAgent 的上下文快照
74
+ *
75
+ * @param {string} currentDimId — 当前维度 ID
76
+ * @returns {DimensionContextSnapshot}
77
+ */
78
+ buildContextForDimension(currentDimId) {
79
+ const previousDimensions = {};
80
+ for (const [id, digest] of this.completedDimensions) {
81
+ previousDimensions[id] = {
82
+ summary: digest.summary,
83
+ candidateCount: digest.candidateCount,
84
+ keyFindings: digest.keyFindings || [],
85
+ crossRefs: digest.crossRefs || {},
86
+ gaps: digest.gaps || [],
87
+ remainingTasks: digest.remainingTasks || [],
88
+ };
89
+ }
90
+
91
+ return {
92
+ project: this.projectContext,
93
+ previousDimensions,
94
+ existingCandidates: this.submittedCandidates.map(c => ({
95
+ dimId: c.dimId,
96
+ title: c.title,
97
+ subTopic: c.subTopic,
98
+ })),
99
+ currentDimension: currentDimId,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * 重算某维度时,获取该维度已有候选
105
+ *
106
+ * @param {string} dimId
107
+ * @returns {Array<CandidateSummary>}
108
+ */
109
+ getExistingCandidatesForDimension(dimId) {
110
+ return this.submittedCandidates.filter(c => c.dimId === dimId);
111
+ }
112
+
113
+ /**
114
+ * 获取所有维度摘要的紧凑文本表示
115
+ * 用于 ChatAgent prompt 中注入
116
+ *
117
+ * @returns {string}
118
+ */
119
+ getDigestsSummaryText() {
120
+ if (this.completedDimensions.size === 0) return '(尚无已完成维度)';
121
+
122
+ const lines = [];
123
+ for (const [id, digest] of this.completedDimensions) {
124
+ lines.push(`### ${id}`);
125
+ lines.push(`- 摘要: ${digest.summary || '(无)'}`);
126
+ lines.push(`- 产出候选: ${digest.candidateCount || 0} 条`);
127
+ if (digest.keyFindings?.length) {
128
+ lines.push(`- 关键发现: ${digest.keyFindings.join('; ')}`);
129
+ }
130
+ if (digest.crossRefs && Object.keys(digest.crossRefs).length > 0) {
131
+ for (const [targetDim, suggestion] of Object.entries(digest.crossRefs)) {
132
+ lines.push(`- → ${targetDim}: ${suggestion}`);
133
+ }
134
+ }
135
+ if (digest.gaps?.length) {
136
+ lines.push(`- 缺口: ${digest.gaps.join('; ')}`);
137
+ }
138
+ if (digest.remainingTasks?.length) {
139
+ lines.push(`- 遗留任务: ${digest.remainingTasks.map(t => t.signal || t).join('; ')}`);
140
+ }
141
+ lines.push('');
142
+ }
143
+ return lines.join('\n');
144
+ }
145
+
146
+ /**
147
+ * 将完整上下文序列化为 JSON (用于断点恢复)
148
+ * @returns {object}
149
+ */
150
+ toJSON() {
151
+ return {
152
+ projectContext: this.projectContext,
153
+ completedDimensions: Object.fromEntries(this.completedDimensions),
154
+ submittedCandidates: this.submittedCandidates,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * 从 JSON 恢复上下文 (断点恢复)
160
+ * @param {object} json
161
+ * @returns {DimensionContext}
162
+ */
163
+ static fromJSON(json) {
164
+ const ctx = new DimensionContext(json.projectContext);
165
+ for (const [id, digest] of Object.entries(json.completedDimensions || {})) {
166
+ ctx.completedDimensions.set(id, digest);
167
+ }
168
+ ctx.submittedCandidates = json.submittedCandidates || [];
169
+ return ctx;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 从 ChatAgent 的最终回复中解析 DimensionDigest
175
+ *
176
+ * ChatAgent 被要求在回复末尾包含 JSON 格式的 dimensionDigest。
177
+ * 此函数从自由格式文本中提取该 JSON 块。
178
+ *
179
+ * @param {string} reply — ChatAgent 的完整回复文本
180
+ * @returns {DimensionDigest|null}
181
+ */
182
+ export function parseDimensionDigest(reply) {
183
+ if (!reply || typeof reply !== 'string') return null;
184
+
185
+ // 尝试匹配 {"dimensionDigest": {...}} 格式
186
+ const jsonBlockRe = /```(?:json)?\s*\n?\s*(\{[\s\S]*?"dimensionDigest"[\s\S]*?\})\s*\n?\s*```/;
187
+ let match = reply.match(jsonBlockRe);
188
+
189
+ // 备选: 没有 code fence 的裸 JSON
190
+ if (!match) {
191
+ const bareRe = /(\{"dimensionDigest"\s*:\s*\{[\s\S]*?\}\s*\})/;
192
+ match = reply.match(bareRe);
193
+ }
194
+
195
+ if (!match) return null;
196
+
197
+ try {
198
+ const parsed = JSON.parse(match[1]);
199
+ const digest = parsed.dimensionDigest || parsed;
200
+
201
+ // 验证必要字段
202
+ if (!digest.summary && !digest.candidateCount) return null;
203
+
204
+ return {
205
+ summary: digest.summary || '',
206
+ candidateCount: digest.candidateCount || 0,
207
+ candidateTitles: digest.candidateTitles || [],
208
+ keyFindings: digest.keyFindings || [],
209
+ crossRefs: digest.crossRefs || {},
210
+ gaps: digest.gaps || [],
211
+ remainingTasks: digest.remainingTasks || [],
212
+ };
213
+ } catch {
214
+ return null;
215
+ }
216
+ }