autosnippet 2.5.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.
- package/bin/cli.js +35 -0
- package/dashboard/dist/assets/{icons-Dtm0E6DS.js → icons-Cq4-iQhP.js} +152 -87
- package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
- package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -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 +1 -1
- package/lib/cli/SetupService.js +33 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/Gateway.js +19 -4
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +117 -10
- package/lib/external/ai/providers/ClaudeProvider.js +197 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
- package/lib/external/ai/providers/OpenAiProvider.js +131 -0
- package/lib/external/mcp/McpServer.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +151 -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 +126 -31
- package/lib/external/mcp/tools.js +25 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +3 -1
- 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 +16 -22
- package/lib/http/routes/skills.js +40 -3
- 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/audit/AuditStore.js +18 -0
- package/lib/infrastructure/config/Paths.js +9 -0
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
- package/lib/injection/ServiceContainer.js +62 -3
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +68 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +216 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1272 -155
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +377 -0
- package/lib/service/chat/HandoffProtocol.js +180 -0
- package/lib/service/chat/Memory.js +40 -10
- package/lib/service/chat/ProducerAgent.js +240 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1493 -60
- package/lib/service/recipe/RecipeFileWriter.js +12 -1
- package/lib/service/skills/EventAggregator.js +187 -0
- package/lib/service/skills/SignalCollector.js +549 -0
- package/lib/service/skills/SkillAdvisor.js +324 -0
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/package.json +1 -1
- 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-B7VpZOCz.css +0 -1
- package/dashboard/dist/assets/index-D87IZTmZ.js +0 -187
|
@@ -11,6 +11,13 @@ export class AiProvider {
|
|
|
11
11
|
this.timeout = config.timeout || 300_000; // 5min
|
|
12
12
|
this.maxRetries = config.maxRetries || 3;
|
|
13
13
|
this.name = 'abstract';
|
|
14
|
+
|
|
15
|
+
// ── CircuitBreaker 状态 ──
|
|
16
|
+
this._circuitState = 'CLOSED'; // 'CLOSED' | 'OPEN' | 'HALF_OPEN'
|
|
17
|
+
this._circuitFailures = 0; // 连续失败计数
|
|
18
|
+
this._circuitThreshold = config.circuitThreshold || 5; // 触发熔断的连续失败次数
|
|
19
|
+
this._circuitOpenedAt = 0; // 熔断打开时间
|
|
20
|
+
this._circuitCooldownMs = 30_000; // 初始冷却 30 秒
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
/**
|
|
@@ -59,6 +66,53 @@ export class AiProvider {
|
|
|
59
66
|
return true;
|
|
60
67
|
}
|
|
61
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
|
+
|
|
62
116
|
/**
|
|
63
117
|
* 从源码文件批量提取 Recipe 结构(AI 驱动)
|
|
64
118
|
* 默认实现使用 chat() + 标准提示词;子类可覆盖以使用专用 API
|
|
@@ -365,7 +419,7 @@ Do NOT overwrite fields that are already filled (listed under "Already filled").
|
|
|
365
419
|
# Fields to Fill (only if missing)
|
|
366
420
|
|
|
367
421
|
1. **rationale** (string): Why this pattern exists; what design intent or problem it solves. 2-3 sentences.
|
|
368
|
-
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".
|
|
369
423
|
3. **complexity** (string): "beginner" | "intermediate" | "advanced". Evaluate usage difficulty.
|
|
370
424
|
4. **scope** (string): "universal" (reusable anywhere) | "project-specific" (specific to this project) | "target-specific" (specific to one module/target).
|
|
371
425
|
5. **steps** (array): Implementation steps. Each: { "title": "Step N title", "description": "What to do", "code": "optional code" }.
|
|
@@ -560,23 +614,76 @@ ${items}`;
|
|
|
560
614
|
}
|
|
561
615
|
|
|
562
616
|
/**
|
|
563
|
-
* 指数退避重试
|
|
617
|
+
* 指数退避重试 + 熔断器(受 Cline 三级错误恢复启发)
|
|
618
|
+
*
|
|
619
|
+
* 熔断器三态:
|
|
620
|
+
* CLOSED — 正常工作,计数连续失败
|
|
621
|
+
* OPEN — 连续 N 次失败,直接拒绝请求(快速失败),持续 cooldownMs
|
|
622
|
+
* HALF_OPEN — 冷却期后尝试一次,成功则恢复,失败则重新 OPEN
|
|
623
|
+
*
|
|
624
|
+
* 这避免了 AI 服务宕机时无意义的重试风暴。
|
|
564
625
|
*/
|
|
565
626
|
async _withRetry(fn, retries = this.maxRetries, baseDelay = 2000) {
|
|
627
|
+
// ── 熔断器检查 ──
|
|
628
|
+
if (this._circuitState === 'OPEN') {
|
|
629
|
+
const elapsed = Date.now() - (this._circuitOpenedAt || 0);
|
|
630
|
+
if (elapsed < (this._circuitCooldownMs || 30000)) {
|
|
631
|
+
const err = new Error(`AI 服务熔断中 (连续 ${this._circuitFailures} 次失败),${Math.ceil(((this._circuitCooldownMs || 30000) - elapsed) / 1000)}s 后恢复`);
|
|
632
|
+
err.code = 'CIRCUIT_OPEN';
|
|
633
|
+
throw err;
|
|
634
|
+
}
|
|
635
|
+
// 冷却期结束 → HALF_OPEN
|
|
636
|
+
this._circuitState = 'HALF_OPEN';
|
|
637
|
+
}
|
|
638
|
+
|
|
566
639
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
567
640
|
try {
|
|
568
|
-
|
|
641
|
+
const result = await fn();
|
|
642
|
+
// 成功 → 完全重置熔断器(包括冷却时间)
|
|
643
|
+
this._circuitFailures = 0;
|
|
644
|
+
this._circuitState = 'CLOSED';
|
|
645
|
+
this._circuitCooldownMs = 30_000; // 重置冷却时间
|
|
646
|
+
return result;
|
|
569
647
|
} catch (err) {
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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'}`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (attempt >= retries || !isRetryable) {
|
|
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
|
+
}
|
|
575
682
|
}
|
|
683
|
+
throw err;
|
|
576
684
|
}
|
|
577
|
-
const isRetryable = err.status === 429 || err.status === 503 || err.code === 'ECONNRESET';
|
|
578
|
-
if (attempt >= retries || !isRetryable) throw err;
|
|
579
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…`);
|
|
580
687
|
await new Promise(r => setTimeout(r, delay));
|
|
581
688
|
}
|
|
582
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,191 @@ 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
|
+
if (!data?.content?.length) {
|
|
207
|
+
return { text: '', functionCalls: null };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const functionCalls = [];
|
|
211
|
+
const textParts = [];
|
|
212
|
+
|
|
213
|
+
for (const block of data.content) {
|
|
214
|
+
if (block.type === 'tool_use') {
|
|
215
|
+
functionCalls.push({
|
|
216
|
+
id: block.id,
|
|
217
|
+
name: block.name,
|
|
218
|
+
args: block.input || {},
|
|
219
|
+
});
|
|
220
|
+
} else if (block.type === 'text') {
|
|
221
|
+
textParts.push(block.text);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (functionCalls.length > 0) {
|
|
226
|
+
this.logger.debug(`[Claude] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
|
|
227
|
+
return {
|
|
228
|
+
text: textParts.length > 0 ? textParts.join('\n') : null,
|
|
229
|
+
functionCalls,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
text: textParts.join('\n'),
|
|
235
|
+
functionCalls: null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
42
239
|
async summarize(code) {
|
|
43
240
|
const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
|
|
44
241
|
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,235 @@ 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
|
+
if (!content || !content.parts || content.parts.length === 0) {
|
|
246
|
+
return { text: '', functionCalls: null };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const functionCalls = [];
|
|
250
|
+
const textParts = [];
|
|
251
|
+
let fcIndex = 0;
|
|
252
|
+
|
|
253
|
+
for (const part of content.parts) {
|
|
254
|
+
if (part.functionCall) {
|
|
255
|
+
functionCalls.push({
|
|
256
|
+
id: `gemini_fc_${Date.now()}_${fcIndex++}`,
|
|
257
|
+
name: part.functionCall.name,
|
|
258
|
+
args: part.functionCall.args || {},
|
|
259
|
+
});
|
|
260
|
+
} else if (part.text) {
|
|
261
|
+
textParts.push(part.text);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (functionCalls.length > 0) {
|
|
266
|
+
this.logger.debug(`[GeminiProvider] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
|
|
267
|
+
return {
|
|
268
|
+
text: textParts.length > 0 ? textParts.join('\n') : null,
|
|
269
|
+
functionCalls,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
text: textParts.join('\n'),
|
|
275
|
+
functionCalls: null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
45
279
|
async summarize(code) {
|
|
46
280
|
const prompt = `请对以下代码生成结构化摘要,返回 JSON 格式 {title, description, language, patterns: [], keyAPIs: []}:\n\n${code}`;
|
|
47
281
|
const text = await this.chat(prompt, { temperature: 0.3 });
|