Files
zhuyongxin cefabd68dc v1.4 工具集扩展 + Windows 编码攻坚
- 新增 BashTool(bash 命令执行,30s 超时 + 自愈 + 截断)
- 新增 WriteFileTool(文件写入)
- Windows GBK 编码自动转 UTF-8(golang.org/x/text)
- OpenAIProvider 新增 API 交互日志
- dumpMessages 移除截断,完整展示上下文
- cmd/claw 三段式任务演示
2026-05-15 17:11:43 +08:00

214 lines
6.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// internal/provider/openai.go
package provider
import (
"context"
"encoding/json"
"fmt"
"log"
// "os"
"go-tiny-claw/internal/schema"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/packages/param"
"github.com/openai/openai-go/v3/shared"
)
type OpenAIProvider struct {
client openai.Client // 值类型,非指针
model string
}
// NewZhipuOpenAIProvider 构造函数:基于 OpenAI V3 SDK,指向智谱底座
func DeepseekOpenAIProvider(model string) *OpenAIProvider {
apiKey := "sk-1f44696abe644bd684f09cc43f12c557"
if apiKey == "" {
panic("请设置 ZHIPU_API_KEY 环境变量")
}
// 核心:将官方 SDK 的地址替换为智谱的兼容端点
baseURL := "https://api.deepseek.com"
return &OpenAIProvider{
client: openai.NewClient(option.WithAPIKey(apiKey), option.WithBaseURL(baseURL)),
model: model,
}
}
func (p *OpenAIProvider) Generate(ctx context.Context, msgs []schema.Message, availableTools []schema.ToolDefinition) (*schema.Message, error) {
phase := "Action"
if len(availableTools) == 0 {
phase = "Thinking"
}
log.Printf("[Provider] → API 调用 | 模型: %s | 阶段: %s | 消息数: %d | 工具数: %d",
p.model, phase, len(msgs), len(availableTools))
var openaiMsgs []openai.ChatCompletionMessageParamUnion
// 1. 翻译上下文消息
for _, msg := range msgs {
switch msg.Role {
case schema.RoleSystem:
openaiMsgs = append(openaiMsgs, openai.SystemMessage(msg.Content))
case schema.RoleUser:
if msg.ToolCallID != "" {
// 注意:v3 新版参数顺序是 (content, toolCallID)
openaiMsgs = append(openaiMsgs, openai.ToolMessage(msg.Content, msg.ToolCallID))
} else {
openaiMsgs = append(openaiMsgs, openai.UserMessage(msg.Content))
}
case schema.RoleAssistant:
// Deepseek thinking mode: reasoning_content 必须回传
if msg.ReasoningContent != "" || len(msg.ToolCalls) > 0 {
msgMap := map[string]interface{}{
"role": "assistant",
"content": msg.Content,
}
if msg.ReasoningContent != "" {
msgMap["reasoning_content"] = msg.ReasoningContent
}
if len(msg.ToolCalls) > 0 {
var toolCalls []map[string]interface{}
for _, tc := range msg.ToolCalls {
toolCalls = append(toolCalls, map[string]interface{}{
"id": tc.ID,
"type": "function",
"function": map[string]interface{}{
"name": tc.Name,
"arguments": string(tc.Arguments),
},
})
}
msgMap["tool_calls"] = toolCalls
}
rawJSON, _ := json.Marshal(msgMap)
astParam := param.Override[openai.ChatCompletionAssistantMessageParam](json.RawMessage(rawJSON))
openaiMsgs = append(openaiMsgs, openai.ChatCompletionMessageParamUnion{
OfAssistant: &astParam,
})
break
}
astParam := openai.ChatCompletionAssistantMessageParam{}
if msg.Content != "" {
astParam.Content = openai.ChatCompletionAssistantMessageParamContentUnion{
OfString: openai.String(msg.Content),
}
}
// 【重要】如果历史包含 ToolCalls,必须原样放回,以维系大模型的逻辑链
if len(msg.ToolCalls) > 0 {
var toolCalls []openai.ChatCompletionMessageToolCallUnionParam
for _, tc := range msg.ToolCalls {
// OfFunction 对应 GetFunction(),字段类型严格要求为指针
toolCalls = append(toolCalls, openai.ChatCompletionMessageToolCallUnionParam{
OfFunction: &openai.ChatCompletionMessageFunctionToolCallParam{
ID: tc.ID,
Type: "function",
Function: openai.ChatCompletionMessageFunctionToolCallFunctionParam{
Name: tc.Name,
Arguments: string(tc.Arguments),
},
},
})
}
astParam.ToolCalls = toolCalls
}
openaiMsgs = append(openaiMsgs, openai.ChatCompletionMessageParamUnion{
OfAssistant: &astParam,
})
}
}
// 2. 翻译工具定义 (v3 新 API 特性适配)
var openaiTools []openai.ChatCompletionToolUnionParam
for _, toolDef := range availableTools {
var params shared.FunctionParameters
// 尝试直接断言,如果不成功则通过 JSON 往返序列化来保证类型匹配
if m, ok := toolDef.InputSchema.(map[string]interface{}); ok {
params = shared.FunctionParameters(m)
} else {
// fallback:JSON 往返序列化
b, _ := json.Marshal(toolDef.InputSchema)
_ = json.Unmarshal(b, &params)
}
openaiTools = append(openaiTools, openai.ChatCompletionFunctionTool(
shared.FunctionDefinitionParam{
Name: toolDef.Name,
Description: openai.String(toolDef.Description),
Parameters: params,
},
))
}
// 3. 构建请求并发送
params := openai.ChatCompletionNewParams{
Model: p.model,
Messages: openaiMsgs,
}
// 【慢思考机制支撑】仅当 availableTools 存在时才挂载 Tools
if len(openaiTools) > 0 {
params.Tools = openaiTools
}
resp, err := p.client.Chat.Completions.New(ctx, params)
if err != nil {
return nil, fmt.Errorf("OpenAI/Zhipu API 请求失败: %w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("API 返回了空的 Choices")
}
// 4. 将 API Response 反向翻译为内部 schema.Message
choice := resp.Choices[0].Message
resultMsg := &schema.Message{
Role: schema.RoleAssistant,
Content: choice.Content,
}
// Deepseek thinking mode: 从原始响应中提取 reasoning_content
rawMsg := choice.RawJSON()
if rawMsg != "" {
var rawMap map[string]json.RawMessage
if err := json.Unmarshal([]byte(rawMsg), &rawMap); err == nil {
if rc, ok := rawMap["reasoning_content"]; ok {
var s string
if json.Unmarshal(rc, &s) == nil {
resultMsg.ReasoningContent = s
}
}
}
}
for _, tc := range choice.ToolCalls {
if tc.Type == "function" {
resultMsg.ToolCalls = append(resultMsg.ToolCalls, schema.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Arguments: []byte(tc.Function.Arguments),
})
}
}
// 打印响应摘要
contentPreview := resultMsg.Content
// if len(contentPreview) > 60 {
// contentPreview = contentPreview[:60] + "..."
// }
log.Printf("[Provider] ← API 响应 | 内容: %q | 工具调用: %d", contentPreview, len(resultMsg.ToolCalls))
for _, tc := range resultMsg.ToolCalls {
log.Printf("[Provider] ↳ ToolCall: %s(%s)", tc.Name, string(tc.Arguments))
}
return resultMsg, nil
}