autosnippet 2.6.0 → 2.7.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 (78) hide show
  1. package/README.md +137 -65
  2. package/bin/api-server.js +5 -0
  3. package/bin/cli.js +6 -1
  4. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
  5. package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
  6. package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
  7. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  8. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  9. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  10. package/dashboard/dist/index.html +6 -6
  11. package/lib/bootstrap.js +18 -1
  12. package/lib/cli/SetupService.js +86 -8
  13. package/lib/cli/UpgradeService.js +139 -2
  14. package/lib/core/ast/ProjectGraph.js +599 -0
  15. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  16. package/lib/domain/recipe/Recipe.js +3 -0
  17. package/lib/external/ai/AiProvider.js +83 -20
  18. package/lib/external/ai/providers/ClaudeProvider.js +208 -0
  19. package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
  20. package/lib/external/ai/providers/OpenAiProvider.js +141 -0
  21. package/lib/external/mcp/McpServer.js +6 -1
  22. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  23. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
  24. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
  25. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  26. package/lib/external/mcp/handlers/bootstrap.js +159 -1634
  27. package/lib/external/mcp/handlers/browse.js +1 -1
  28. package/lib/external/mcp/handlers/candidate.js +1 -33
  29. package/lib/external/mcp/handlers/skill.js +58 -17
  30. package/lib/external/mcp/tools.js +4 -3
  31. package/lib/http/middleware/requestLogger.js +23 -4
  32. package/lib/http/routes/ai.js +158 -2
  33. package/lib/http/routes/auth.js +3 -2
  34. package/lib/http/routes/candidates.js +49 -25
  35. package/lib/http/routes/commands.js +0 -8
  36. package/lib/http/routes/guardRules.js +1 -16
  37. package/lib/http/routes/recipes.js +4 -17
  38. package/lib/http/routes/search.js +11 -19
  39. package/lib/http/routes/skills.js +2 -0
  40. package/lib/http/routes/snippets.js +0 -33
  41. package/lib/http/routes/spm.js +37 -63
  42. package/lib/http/utils/routeHelpers.js +31 -0
  43. package/lib/infrastructure/config/Paths.js +12 -0
  44. package/lib/infrastructure/database/DatabaseConnection.js +6 -1
  45. package/lib/infrastructure/logging/Logger.js +86 -3
  46. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  47. package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
  48. package/lib/injection/ServiceContainer.js +55 -2
  49. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  50. package/lib/service/candidate/CandidateFileWriter.js +72 -27
  51. package/lib/service/candidate/CandidateService.js +156 -10
  52. package/lib/service/chat/AnalystAgent.js +245 -0
  53. package/lib/service/chat/CandidateGuardrail.js +134 -0
  54. package/lib/service/chat/ChatAgent.js +1055 -167
  55. package/lib/service/chat/ContextWindow.js +730 -0
  56. package/lib/service/chat/ConversationStore.js +3 -0
  57. package/lib/service/chat/HandoffProtocol.js +181 -0
  58. package/lib/service/chat/Memory.js +3 -0
  59. package/lib/service/chat/ProducerAgent.js +293 -0
  60. package/lib/service/chat/ToolRegistry.js +149 -5
  61. package/lib/service/chat/tools.js +1404 -61
  62. package/lib/service/guard/ExclusionManager.js +2 -0
  63. package/lib/service/guard/RuleLearner.js +2 -0
  64. package/lib/service/quality/FeedbackCollector.js +2 -0
  65. package/lib/service/recipe/RecipeFileWriter.js +16 -1
  66. package/lib/service/recipe/RecipeStatsTracker.js +2 -0
  67. package/lib/service/skills/SignalCollector.js +33 -6
  68. package/lib/service/skills/SkillAdvisor.js +2 -1
  69. package/lib/service/skills/SkillHooks.js +13 -5
  70. package/lib/service/spm/SpmService.js +2 -2
  71. package/lib/shared/PathGuard.js +314 -0
  72. package/package.json +1 -1
  73. package/resources/native-ui/combined-window.swift +494 -0
  74. package/templates/copilot-instructions.md +20 -3
  75. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  76. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  77. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  78. package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
@@ -66,6 +66,53 @@ export class AiProvider {
66
66
  return true;
67
67
  }
68
68
 
69
+ /**
70
+ * 是否支持原生结构化函数调用(非文本解析)
71
+ * 子类(如 GoogleGeminiProvider)覆盖返回 true
72
+ * @returns {boolean}
73
+ */
74
+ get supportsNativeToolCalling() {
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * 带工具声明的结构化对话 — 原生函数调用 API
80
+ *
81
+ * 支持原生函数调用的 Provider(Gemini / OpenAI / Claude)覆盖此方法,
82
+ * 返回结构化 functionCall 而非文本,ChatAgent 据此跳过正则解析。
83
+ *
84
+ * 默认实现降级为 chat(),由 ChatAgent 进行文本解析。
85
+ *
86
+ * 统一消息格式 (Provider-Agnostic):
87
+ * - { role: 'user', content: 'text' }
88
+ * - { role: 'assistant', content: 'text or null', toolCalls: [{id, name, args}] }
89
+ * - { role: 'tool', toolCallId: 'id', name: 'tool_name', content: 'result string' }
90
+ *
91
+ * @param {string} prompt — 用户消息(仅在 messages 为空时使用)
92
+ * @param {object} opts
93
+ * @param {Array} opts.messages — 统一格式消息历史
94
+ * @param {Array} opts.toolSchemas — [{name, description, parameters}]
95
+ * @param {string} opts.toolChoice — 'auto' | 'required' | 'none'
96
+ * @param {string} [opts.systemPrompt] — 系统指令
97
+ * @param {number} [opts.temperature=0.7]
98
+ * @param {number} [opts.maxTokens=8192]
99
+ * @returns {Promise<{text: string|null, functionCalls: Array<{id: string, name: string, args: object}>|null}>}
100
+ */
101
+ async chatWithTools(prompt, opts = {}) {
102
+ // 默认降级: 忽略 tools/toolChoice,走纯文本 chat()
103
+ const messages = opts.messages || [];
104
+ const history = messages
105
+ .filter(m => m.role === 'user' || m.role === 'assistant')
106
+ .map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content || '' }));
107
+ const text = await this.chat(prompt, {
108
+ history,
109
+ systemPrompt: opts.systemPrompt,
110
+ temperature: opts.temperature,
111
+ maxTokens: opts.maxTokens,
112
+ });
113
+ return { text, functionCalls: null };
114
+ }
115
+
69
116
  /**
70
117
  * 从源码文件批量提取 Recipe 结构(AI 驱动)
71
118
  * 默认实现使用 chat() + 标准提示词;子类可覆盖以使用专用 API
@@ -372,7 +419,7 @@ Do NOT overwrite fields that are already filled (listed under "Already filled").
372
419
  # Fields to Fill (only if missing)
373
420
 
374
421
  1. **rationale** (string): Why this pattern exists; what design intent or problem it solves. 2-3 sentences.
375
- 2. **knowledgeType** (string): One of: "code-standard", "code-pattern", "architecture", "best-practice", "code-relation", "inheritance", "call-chain", "data-flow", "module-dependency", "boundary-constraint", "code-style", "solution".
422
+ 2. **knowledgeType** (string): One of: "code-standard", "code-pattern", "architecture", "best-practice", "code-relation", "inheritance", "call-chain", "data-flow", "module-dependency", "boundary-constraint", "code-style", "solution", "anti-pattern".
376
423
  3. **complexity** (string): "beginner" | "intermediate" | "advanced". Evaluate usage difficulty.
377
424
  4. **scope** (string): "universal" (reusable anywhere) | "project-specific" (specific to this project) | "target-specific" (specific to one module/target).
378
425
  5. **steps** (array): Implementation steps. Each: { "title": "Step N title", "description": "What to do", "code": "optional code" }.
@@ -592,35 +639,51 @@ ${items}`;
592
639
  for (let attempt = 0; attempt <= retries; attempt++) {
593
640
  try {
594
641
  const result = await fn();
595
- // 成功 → 重置熔断器
642
+ // 成功 → 完全重置熔断器(包括冷却时间)
596
643
  this._circuitFailures = 0;
597
644
  this._circuitState = 'CLOSED';
645
+ this._circuitCooldownMs = 30_000; // 重置冷却时间
598
646
  return result;
599
647
  } catch (err) {
600
- // 连接超时提示代理配置
601
- if (err.cause?.code === 'UND_ERR_CONNECT_TIMEOUT' || err.code === 'UND_ERR_CONNECT_TIMEOUT') {
602
- const hasProxy = process.env.ASD_AI_PROXY || process.env.HTTPS_PROXY || process.env.ALL_PROXY;
603
- if (!hasProxy) {
604
- err.message += ' — 💡 可能需要配置代理: export HTTPS_PROXY=http://127.0.0.1:7890';
605
- }
648
+ // ── 综合判断是否为可重试的网络/服务端错误 ──
649
+ const causeCode = err.cause?.code || '';
650
+ // 网络级错误:无 HTTP status,底层连接失败
651
+ const isNetworkError = !err.status && (
652
+ err.message === 'fetch failed'
653
+ || err.code === 'ECONNRESET' || causeCode === 'ECONNRESET'
654
+ || err.code === 'ECONNREFUSED' || causeCode === 'ECONNREFUSED'
655
+ || err.code === 'ENOTFOUND' || causeCode === 'ENOTFOUND'
656
+ || err.code === 'ECONNABORTED' || causeCode === 'ECONNABORTED'
657
+ || err.code === 'ETIMEDOUT' || causeCode === 'ETIMEDOUT'
658
+ || err.code === 'UND_ERR_CONNECT_TIMEOUT' || causeCode === 'UND_ERR_CONNECT_TIMEOUT'
659
+ || err.code === 'UND_ERR_SOCKET' || causeCode === 'UND_ERR_SOCKET'
660
+ );
661
+ const isRetryable = err.status === 429 || err.status >= 500 || isNetworkError;
662
+
663
+ // 首次失败记录详细诊断(含 cause)
664
+ if (attempt === 0 && (isNetworkError || err.cause)) {
665
+ this._log?.('warn', `[_withRetry] ${err.message} — cause: ${err.cause?.message || causeCode || 'unknown'}`);
606
666
  }
607
- const isRetryable = err.status === 429 || err.status === 503 || err.code === 'ECONNRESET';
667
+
608
668
  if (attempt >= retries || !isRetryable) {
609
- // 累计熔断失败计数
610
- this._circuitFailures = (this._circuitFailures || 0) + 1;
611
- if (this._circuitFailures >= (this._circuitThreshold || 5)) {
612
- this._circuitState = 'OPEN';
613
- this._circuitOpenedAt = Date.now();
614
- // 逐级递增冷却时间: 30s 60s → 120s(最大 5 分钟)
615
- this._circuitCooldownMs = Math.min(
616
- (this._circuitCooldownMs || 15000) * 2,
617
- 300_000,
618
- );
619
- this._log?.('warn', `[CircuitBreaker] OPEN — ${this._circuitFailures} consecutive failures, cooldown ${this._circuitCooldownMs / 1000}s`);
669
+ // 只有服务端错误 / 网络错误才累计熔断计数
670
+ // 客户端错误 (4xx 429) 不应触发熔断 — 那是请求本身的问题
671
+ const isServerError = isNetworkError || err.status === 429 || err.status >= 500 || !err.status;
672
+ if (isServerError) {
673
+ this._circuitFailures = (this._circuitFailures || 0) + 1;
674
+ if (this._circuitFailures >= (this._circuitThreshold || 5)) {
675
+ this._circuitState = 'OPEN';
676
+ this._circuitOpenedAt = Date.now();
677
+ // 先用当前冷却值,再递增给下次: 30s → 60s → 120s(最大 5 分钟)
678
+ const cooldown = this._circuitCooldownMs || 30_000;
679
+ this._log?.('warn', `[CircuitBreaker] OPEN — ${this._circuitFailures} consecutive failures, cooldown ${cooldown / 1000}s`);
680
+ this._circuitCooldownMs = Math.min(cooldown * 2, 300_000);
681
+ }
620
682
  }
621
683
  throw err;
622
684
  }
623
685
  const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
686
+ this._log?.('info', `[_withRetry] attempt ${attempt + 1} failed (${err.message}), retrying in ${Math.round(delay / 1000)}s…`);
624
687
  await new Promise(r => setTimeout(r, delay));
625
688
  }
626
689
  }
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * ClaudeProvider - Anthropic Claude AI 提供商
3
+ *
4
+ * v2: 支持原生 Function Calling(结构化工具调用)
5
+ * - 使用 Anthropic Messages API 的 tools + tool_choice 参数
6
+ * - 响应中的 tool_use content blocks → 结构化 functionCall
7
+ * - tool_result content blocks 用于回传工具执行结果
3
8
  */
4
9
 
5
10
  import { AiProvider } from '../AiProvider.js';
@@ -18,6 +23,13 @@ export class ClaudeProvider extends AiProvider {
18
23
  this.logger = Logger.getInstance();
19
24
  }
20
25
 
26
+ /**
27
+ * 是否支持原生结构化函数调用
28
+ */
29
+ get supportsNativeToolCalling() {
30
+ return true;
31
+ }
32
+
21
33
  async chat(prompt, context = {}) {
22
34
  const { history = [], temperature = 0.7, maxTokens = 4096 } = context;
23
35
  const messages = [];
@@ -39,6 +51,202 @@ export class ClaudeProvider extends AiProvider {
39
51
  return textBlock?.text || '';
40
52
  }
41
53
 
54
+ /**
55
+ * 带工具声明的结构化对话 — Anthropic Messages API Tool Use
56
+ *
57
+ * 接受统一消息格式,内部转换为 Anthropic Messages 格式。
58
+ *
59
+ * Anthropic 特殊之处:
60
+ * - system prompt 是顶层 `system` 字段(非 message)
61
+ * - assistant 消息的 content 是 content blocks 数组
62
+ * - 工具结果通过 user 消息中的 tool_result blocks 传递
63
+ * - tool_choice: {type: 'auto'|'any'|'tool'}(无 'none',不传 tools 即可)
64
+ *
65
+ * @param {string} prompt — fallback prompt
66
+ * @param {object} opts — 统一参数
67
+ * @returns {Promise<{text: string|null, functionCalls: Array<{id, name, args}>|null}>}
68
+ */
69
+ async chatWithTools(prompt, opts = {}) {
70
+ const {
71
+ messages: unifiedMessages,
72
+ toolSchemas,
73
+ toolChoice = 'auto',
74
+ systemPrompt,
75
+ temperature = 0.7,
76
+ maxTokens = 4096,
77
+ } = opts;
78
+
79
+ // 统一消息 → Anthropic Messages 格式
80
+ const srcMessages = unifiedMessages?.length > 0
81
+ ? unifiedMessages
82
+ : [{ role: 'user', content: prompt }];
83
+
84
+ const messages = this.#convertMessages(srcMessages);
85
+
86
+ const body = {
87
+ model: this.model,
88
+ messages,
89
+ max_tokens: maxTokens,
90
+ temperature,
91
+ };
92
+
93
+ // system prompt → 顶层字段
94
+ if (systemPrompt) {
95
+ body.system = systemPrompt;
96
+ }
97
+
98
+ // 工具声明 + tool_choice
99
+ // toolChoice='none' 时不传 tools(Anthropic 没有 'none' tool_choice)
100
+ if (toolChoice !== 'none' && toolSchemas?.length > 0) {
101
+ body.tools = toolSchemas.map(s => ({
102
+ name: s.name,
103
+ description: s.description || '',
104
+ input_schema: s.parameters || { type: 'object', properties: {} },
105
+ }));
106
+
107
+ if (toolChoice === 'required') {
108
+ body.tool_choice = { type: 'any' };
109
+ } else {
110
+ body.tool_choice = { type: 'auto' };
111
+ }
112
+ }
113
+
114
+ const data = await this._post(`${CLAUDE_BASE}/messages`, body);
115
+ return this.#parseToolResponse(data);
116
+ }
117
+
118
+ // ─── 内部转换方法 ──────────────────────
119
+
120
+ /**
121
+ * 统一消息格式 → Anthropic Messages 格式
122
+ *
123
+ * - user → {role: 'user', content: 'text'}
124
+ * - assistant → {role: 'assistant', content: [{type:'text'}, {type:'tool_use'}...]}
125
+ * - tool → grouped into {role: 'user', content: [{type:'tool_result'}...]}
126
+ *
127
+ * Anthropic 要求消息交替 user/assistant。连续 tool results 合并为一个 user 消息。
128
+ * 连续同角色消息(如 L2/L3 压缩后的摘要)自动合并以避免 400 错误。
129
+ */
130
+ #convertMessages(messages) {
131
+ const result = [];
132
+
133
+ /**
134
+ * 推入 result,如果上一个 entry 同角色则合并 content
135
+ */
136
+ const pushOrMerge = (entry) => {
137
+ const last = result[result.length - 1];
138
+ if (last && last.role === entry.role) {
139
+ // Anthropic content 可以是 string 或 array
140
+ const lastContent = Array.isArray(last.content)
141
+ ? last.content
142
+ : [{ type: 'text', text: last.content || '' }];
143
+ const newContent = Array.isArray(entry.content)
144
+ ? entry.content
145
+ : [{ type: 'text', text: entry.content || '' }];
146
+ last.content = [...lastContent, ...newContent];
147
+ } else {
148
+ result.push(entry);
149
+ }
150
+ };
151
+
152
+ let i = 0;
153
+
154
+ while (i < messages.length) {
155
+ const msg = messages[i];
156
+
157
+ if (msg.role === 'user') {
158
+ pushOrMerge({ role: 'user', content: msg.content || '' });
159
+ i++;
160
+ } else if (msg.role === 'assistant') {
161
+ const content = [];
162
+ if (msg.content) content.push({ type: 'text', text: msg.content });
163
+ if (msg.toolCalls?.length > 0) {
164
+ for (const tc of msg.toolCalls) {
165
+ content.push({
166
+ type: 'tool_use',
167
+ id: tc.id,
168
+ name: tc.name,
169
+ input: tc.args || {},
170
+ });
171
+ }
172
+ }
173
+ pushOrMerge({
174
+ role: 'assistant',
175
+ content: content.length > 0 ? content : [{ type: 'text', text: '' }],
176
+ });
177
+ i++;
178
+ } else if (msg.role === 'tool') {
179
+ // 收集连续 tool results → 合并为一个 user 消息
180
+ const toolResults = [];
181
+ while (i < messages.length && messages[i].role === 'tool') {
182
+ toolResults.push({
183
+ type: 'tool_result',
184
+ tool_use_id: messages[i].toolCallId,
185
+ content: messages[i].content || '',
186
+ });
187
+ i++;
188
+ }
189
+ pushOrMerge({ role: 'user', content: toolResults });
190
+ } else {
191
+ i++; // skip unknown roles
192
+ }
193
+ }
194
+
195
+ return result;
196
+ }
197
+
198
+ /**
199
+ * 解析 Anthropic Messages API 响应 — 提取 tool_use 或 text
200
+ *
201
+ * Anthropic 返回格式:
202
+ * content[]: { type: 'text', text } | { type: 'tool_use', id, name, input }
203
+ * stop_reason: 'end_turn' | 'tool_use' | 'max_tokens'
204
+ */
205
+ #parseToolResponse(data) {
206
+ // 提取 token 用量 (Claude usage)
207
+ const usage = data?.usage
208
+ ? {
209
+ inputTokens: data.usage.input_tokens || 0,
210
+ outputTokens: data.usage.output_tokens || 0,
211
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
212
+ }
213
+ : null;
214
+
215
+ if (!data?.content?.length) {
216
+ return { text: '', functionCalls: null, usage };
217
+ }
218
+
219
+ const functionCalls = [];
220
+ const textParts = [];
221
+
222
+ for (const block of data.content) {
223
+ if (block.type === 'tool_use') {
224
+ functionCalls.push({
225
+ id: block.id,
226
+ name: block.name,
227
+ args: block.input || {},
228
+ });
229
+ } else if (block.type === 'text') {
230
+ textParts.push(block.text);
231
+ }
232
+ }
233
+
234
+ if (functionCalls.length > 0) {
235
+ this.logger.debug(`[Claude] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
236
+ return {
237
+ text: textParts.length > 0 ? textParts.join('\n') : null,
238
+ functionCalls,
239
+ usage,
240
+ };
241
+ }
242
+
243
+ return {
244
+ text: textParts.join('\n'),
245
+ functionCalls: null,
246
+ usage,
247
+ };
248
+ }
249
+
42
250
  async summarize(code) {
43
251
  const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
44
252
  const text = await this.chat(prompt, { temperature: 0.3 });
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * GoogleGeminiProvider - Google Gemini AI 提供商
3
3
  * 直接调用 REST API(不依赖 SDK)
4
+ *
5
+ * v3: 统一消息格式 — chatWithTools() 接受 Provider-Agnostic 消息
6
+ * 内部自动转换为 Gemini 原生 contents / functionDeclarations 格式
7
+ * 支持 toolChoice: 'auto' | 'required' | 'none'
4
8
  */
5
9
 
6
10
  import { AiProvider } from '../AiProvider.js';
@@ -18,9 +22,16 @@ export class GoogleGeminiProvider extends AiProvider {
18
22
  this.logger = Logger.getInstance();
19
23
  }
20
24
 
25
+ /**
26
+ * 是否支持原生结构化函数调用
27
+ */
28
+ get supportsNativeToolCalling() {
29
+ return true;
30
+ }
31
+
21
32
  async chat(prompt, context = {}) {
22
33
  return this._withRetry(async () => {
23
- const { history = [], temperature = 0.7, maxTokens = 8192 } = context;
34
+ const { history = [], temperature = 0.7, maxTokens = 8192, systemPrompt } = context;
24
35
  const contents = [];
25
36
 
26
37
  for (const h of history) {
@@ -36,12 +47,247 @@ export class GoogleGeminiProvider extends AiProvider {
36
47
  },
37
48
  };
38
49
 
50
+ // systemInstruction 支持(chat 也可用 systemPrompt)
51
+ if (systemPrompt) {
52
+ body.systemInstruction = { parts: [{ text: systemPrompt }] };
53
+ }
54
+
39
55
  const url = `${GEMINI_BASE}/models/${this.model}:generateContent?key=${this.apiKey}`;
40
56
  const data = await this._post(url, body);
41
57
  return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
42
58
  });
43
59
  }
44
60
 
61
+ /**
62
+ * 带工具声明的结构化对话 — Gemini 原生 Function Calling
63
+ *
64
+ * 接受统一消息格式,内部转换为 Gemini 原生 contents 格式。
65
+ *
66
+ * @param {string} prompt — 未使用 messages 时的 fallback prompt
67
+ * @param {object} opts
68
+ * @param {Array} opts.messages — 统一格式消息
69
+ * @param {Array} opts.toolSchemas — [{name, description, parameters}]
70
+ * @param {string} opts.toolChoice — 'auto' | 'required' | 'none'
71
+ * @param {string} [opts.systemPrompt]
72
+ * @param {number} [opts.temperature=0.7]
73
+ * @param {number} [opts.maxTokens=8192]
74
+ * @returns {Promise<{text: string|null, functionCalls: Array<{id, name, args}>|null}>}
75
+ */
76
+ async chatWithTools(prompt, opts = {}) {
77
+ return this._withRetry(async () => {
78
+ const {
79
+ messages,
80
+ toolSchemas,
81
+ toolChoice = 'auto',
82
+ systemPrompt,
83
+ temperature = 0.7,
84
+ maxTokens = 8192,
85
+ } = opts;
86
+
87
+ // 统一消息 → Gemini contents
88
+ const contents = messages?.length > 0
89
+ ? this.#convertMessages(messages)
90
+ : [{ role: 'user', parts: [{ text: prompt }] }];
91
+
92
+ const body = {
93
+ contents,
94
+ generationConfig: {
95
+ temperature,
96
+ maxOutputTokens: maxTokens,
97
+ },
98
+ };
99
+
100
+ // 工具声明: 标准 schema → Gemini functionDeclarations
101
+ if (toolSchemas?.length > 0) {
102
+ body.tools = [{
103
+ functionDeclarations: toolSchemas.map(s => this.#toFunctionDeclaration(s)),
104
+ }];
105
+ }
106
+
107
+ // toolChoice → Gemini mode
108
+ body.toolConfig = {
109
+ functionCallingConfig: { mode: this.#toGeminiMode(toolChoice) },
110
+ };
111
+
112
+ // 系统指令
113
+ if (systemPrompt) {
114
+ body.systemInstruction = { parts: [{ text: systemPrompt }] };
115
+ }
116
+
117
+ const url = `${GEMINI_BASE}/models/${this.model}:generateContent?key=${this.apiKey}`;
118
+ const data = await this._post(url, body);
119
+
120
+ return this.#parseToolResponse(data);
121
+ });
122
+ }
123
+
124
+ // ─── 内部转换方法 ──────────────────────
125
+
126
+ /**
127
+ * 统一消息格式 → Gemini contents
128
+ * - user → {role: 'user', parts: [{text}]}
129
+ * - assistant → {role: 'model', parts: [{text}, {functionCall}...]}
130
+ * - tool → grouped into {role: 'user', parts: [{functionResponse}...]}
131
+ *
132
+ * Gemini 要求严格交替 user/model 角色。
133
+ * 连续同角色消息(如 L2/L3 压缩后的摘要)自动合并 parts 以避免 400 错误。
134
+ */
135
+ #convertMessages(messages) {
136
+ const contents = [];
137
+ let pendingToolResults = [];
138
+
139
+ /**
140
+ * 推入 contents,如果上一个 entry 同角色则合并 parts
141
+ */
142
+ const pushOrMerge = (entry) => {
143
+ const last = contents[contents.length - 1];
144
+ if (last && last.role === entry.role) {
145
+ last.parts.push(...entry.parts);
146
+ } else {
147
+ contents.push(entry);
148
+ }
149
+ };
150
+
151
+ for (const msg of messages) {
152
+ if (msg.role === 'tool') {
153
+ // 收集连续 tool results → 将在下一个非 tool 消息前或末尾 flush
154
+ pendingToolResults.push({
155
+ functionResponse: {
156
+ name: msg.name,
157
+ response: { result: msg.content || '' },
158
+ },
159
+ });
160
+ continue;
161
+ }
162
+
163
+ // Flush pending tool results before non-tool message
164
+ if (pendingToolResults.length > 0) {
165
+ pushOrMerge({ role: 'user', parts: pendingToolResults });
166
+ pendingToolResults = [];
167
+ }
168
+
169
+ if (msg.role === 'user') {
170
+ pushOrMerge({ role: 'user', parts: [{ text: msg.content || '' }] });
171
+ } else if (msg.role === 'assistant') {
172
+ const parts = [];
173
+ if (msg.content) parts.push({ text: msg.content });
174
+ if (msg.toolCalls?.length > 0) {
175
+ for (const tc of msg.toolCalls) {
176
+ parts.push({ functionCall: { name: tc.name, args: tc.args || {} } });
177
+ }
178
+ }
179
+ if (parts.length > 0) pushOrMerge({ role: 'model', parts });
180
+ }
181
+ }
182
+
183
+ // Flush remaining tool results
184
+ if (pendingToolResults.length > 0) {
185
+ pushOrMerge({ role: 'user', parts: pendingToolResults });
186
+ }
187
+
188
+ return contents;
189
+ }
190
+
191
+ /**
192
+ * toolChoice → Gemini mode
193
+ */
194
+ #toGeminiMode(toolChoice) {
195
+ switch (toolChoice) {
196
+ case 'required': return 'ANY';
197
+ case 'none': return 'NONE';
198
+ default: return 'AUTO';
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 标准 tool schema → Gemini functionDeclaration
204
+ */
205
+ #toFunctionDeclaration(schema) {
206
+ return {
207
+ name: schema.name,
208
+ description: schema.description || '',
209
+ parameters: this.#sanitizeSchemaForGemini(schema.parameters),
210
+ };
211
+ }
212
+
213
+ /**
214
+ * 清理 JSON Schema 使之兼容 Gemini API 的 OpenAPI 子集
215
+ */
216
+ #sanitizeSchemaForGemini(schema) {
217
+ if (!schema || typeof schema !== 'object') {
218
+ return { type: 'object', properties: {} };
219
+ }
220
+
221
+ const cleaned = { ...schema };
222
+ if (!cleaned.type) cleaned.type = 'object';
223
+
224
+ if (cleaned.properties) {
225
+ const props = {};
226
+ for (const [key, val] of Object.entries(cleaned.properties)) {
227
+ const prop = { ...val };
228
+ delete prop.default;
229
+ delete prop.examples;
230
+ if (!prop.type) prop.type = 'string';
231
+ props[key] = prop;
232
+ }
233
+ cleaned.properties = props;
234
+ }
235
+
236
+ return cleaned;
237
+ }
238
+
239
+ /**
240
+ * 解析 Gemini API 响应 — 提取 functionCall 或 text
241
+ * 返回统一格式(含生成的 id)
242
+ */
243
+ #parseToolResponse(data) {
244
+ const content = data?.candidates?.[0]?.content;
245
+
246
+ // 提取 token 用量 (Gemini usageMetadata)
247
+ const usage = data?.usageMetadata
248
+ ? {
249
+ inputTokens: data.usageMetadata.promptTokenCount || 0,
250
+ outputTokens: data.usageMetadata.candidatesTokenCount || 0,
251
+ totalTokens: data.usageMetadata.totalTokenCount || 0,
252
+ }
253
+ : null;
254
+
255
+ if (!content || !content.parts || content.parts.length === 0) {
256
+ return { text: '', functionCalls: null, usage };
257
+ }
258
+
259
+ const functionCalls = [];
260
+ const textParts = [];
261
+ let fcIndex = 0;
262
+
263
+ for (const part of content.parts) {
264
+ if (part.functionCall) {
265
+ functionCalls.push({
266
+ id: `gemini_fc_${Date.now()}_${fcIndex++}`,
267
+ name: part.functionCall.name,
268
+ args: part.functionCall.args || {},
269
+ });
270
+ } else if (part.text) {
271
+ textParts.push(part.text);
272
+ }
273
+ }
274
+
275
+ if (functionCalls.length > 0) {
276
+ this.logger.debug(`[GeminiProvider] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
277
+ return {
278
+ text: textParts.length > 0 ? textParts.join('\n') : null,
279
+ functionCalls,
280
+ usage,
281
+ };
282
+ }
283
+
284
+ return {
285
+ text: textParts.join('\n'),
286
+ functionCalls: null,
287
+ usage,
288
+ };
289
+ }
290
+
45
291
  async summarize(code) {
46
292
  const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
47
293
  const text = await this.chat(prompt, { temperature: 0.3 });