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.
- package/README.md +137 -65
- package/bin/api-server.js +5 -0
- package/bin/cli.js +6 -1
- package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
- package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
- package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
- package/dashboard/dist/index.html +6 -6
- package/lib/bootstrap.js +18 -1
- package/lib/cli/SetupService.js +86 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +83 -20
- package/lib/external/ai/providers/ClaudeProvider.js +208 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
- package/lib/external/ai/providers/OpenAiProvider.js +141 -0
- package/lib/external/mcp/McpServer.js +6 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +159 -1634
- package/lib/external/mcp/handlers/browse.js +1 -1
- package/lib/external/mcp/handlers/candidate.js +1 -33
- package/lib/external/mcp/handlers/skill.js +58 -17
- package/lib/external/mcp/tools.js +4 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +158 -2
- package/lib/http/routes/auth.js +3 -2
- package/lib/http/routes/candidates.js +49 -25
- package/lib/http/routes/commands.js +0 -8
- package/lib/http/routes/guardRules.js +1 -16
- package/lib/http/routes/recipes.js +4 -17
- package/lib/http/routes/search.js +11 -19
- package/lib/http/routes/skills.js +2 -0
- package/lib/http/routes/snippets.js +0 -33
- package/lib/http/routes/spm.js +37 -63
- package/lib/http/utils/routeHelpers.js +31 -0
- package/lib/infrastructure/config/Paths.js +12 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
- package/lib/injection/ServiceContainer.js +55 -2
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +72 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +245 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1055 -167
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +181 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +293 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1404 -61
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +16 -1
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +33 -6
- package/lib/service/skills/SkillAdvisor.js +2 -1
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/templates/copilot-instructions.md +20 -3
- package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
- package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
- package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
- 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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
667
|
+
|
|
608
668
|
if (attempt >= retries || !isRetryable) {
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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 });
|