feat(ai): 发布全新 AI Copilot 助手面板与工作区智能打通

- 核心架构:新增独立 AI 会话中枢,集成主流大模型生态(含私有部署中继版)的无感衔接发问
- 智能诊断:打破信息孤岛,大模型可通过关联工作区实时数据表 DDL 和错误栈,充当专属 DBA 排错及代码编写
- 视觉与多模态:支持极简发图读图交互体验,智能补全模型所需的缺省预警 Prompt,并兼容不规范中转端点图文并茂
- UI 与性能:重构聊天浮层挂靠逻辑与渲染阻断,应对长时间巨量问答引发的卡段内存泄漏,会话自动保存归档
This commit is contained in:
Syngnat
2026-03-26 16:02:08 +08:00
parent 82369b4070
commit 98e9e5686d
27 changed files with 4902 additions and 745 deletions

View File

@@ -208,6 +208,7 @@ func buildGeneralChatPrompt() string {
互动守则:
- 永远使用专业、具有合作感且充满信心的中文与用户探讨问题。
- 当被要求提供任何数据库代码时,需结合相关数据库引擎的最佳实践。如果不清楚当前方言版本,请以标准实现为主基调并好心指出版别差异(如 MySQL 8 窗口函数 等)。`
- 当被要求提供任何数据库代码时,需结合相关数据库引擎的最佳实践。如果不清楚当前方言版本,请以标准实现为主基调并好心指出版别差异(如 MySQL 8 窗口函数 等)。
- 绝不轻易拒绝:如果用户要求写 SQL 但并未显式挂载任何表的详细 DDL请尽最大努力根据对话上下文中带入的【纯表名列表】去推测他要查询哪个表。如果实在无法推断请温柔且专业地向用户解释目前已知的表有哪些并询问到底想查哪张表。`
}

View File

@@ -82,8 +82,42 @@ type anthropicRequest struct {
}
type anthropicMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Role string `json:"role"`
Content interface{} `json:"content"`
}
func buildAnthropicMessages(reqMessages []ai.Message) []anthropicMessage {
messages := make([]anthropicMessage, 0, len(reqMessages))
for _, m := range reqMessages {
if len(m.Images) > 0 {
var contentParts []map[string]interface{}
for _, img := range m.Images {
mimeType, rawBase64, err := ParseDataURI(img)
if err == nil {
contentParts = append(contentParts, map[string]interface{}{
"type": "image",
"source": map[string]interface{}{
"type": "base64",
"media_type": mimeType,
"data": rawBase64,
},
})
}
}
text := m.Content
if text == "" {
text = "请描述和分析这张图片。" // 防止强 System Prompt 下模型仅看到空文本且忽略图片直接回复打招呼
}
contentParts = append(contentParts, map[string]interface{}{
"type": "text",
"text": text,
})
messages = append(messages, anthropicMessage{Role: m.Role, Content: contentParts})
} else {
messages = append(messages, anthropicMessage{Role: m.Role, Content: m.Content})
}
}
return messages
}
type anthropicResponse struct {
@@ -112,10 +146,7 @@ func (p *AnthropicProvider) Chat(ctx context.Context, req ai.ChatRequest) (*ai.C
}
systemMsg, messages := extractSystemMessage(req.Messages)
anthropicMsgs := make([]anthropicMessage, len(messages))
for i, m := range messages {
anthropicMsgs[i] = anthropicMessage{Role: m.Role, Content: m.Content}
}
anthropicMsgs := buildAnthropicMessages(messages)
temperature := req.Temperature
if temperature <= 0 {
@@ -167,10 +198,7 @@ func (p *AnthropicProvider) ChatStream(ctx context.Context, req ai.ChatRequest,
}
systemMsg, messages := extractSystemMessage(req.Messages)
anthropicMsgs := make([]anthropicMessage, len(messages))
for i, m := range messages {
anthropicMsgs[i] = anthropicMessage{Role: m.Role, Content: m.Content}
}
anthropicMsgs := buildAnthropicMessages(messages)
temperature := req.Temperature
if temperature <= 0 {
@@ -253,6 +281,12 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, body interface{}) (io
httpReq.Header.Set("x-api-key", p.config.APIKey)
httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
if strings.Contains(string(jsonBody), `"stream":true`) || strings.Contains(string(jsonBody), `"stream": true`) {
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Connection", "keep-alive")
}
// 仅官方 API 发 beta 特性头(代理不发,避免触发 Claude Code 验证)
isOfficialAPI := p.baseURL == defaultAnthropicBaseURL || strings.Contains(p.baseURL, "anthropic.com")
if isOfficialAPI {

View File

@@ -105,8 +105,7 @@ func (p *ClaudeCLIProvider) ChatStream(ctx context.Context, req ai.ChatRequest,
fmt.Printf("[ClaudeCLI DEBUG] Process started, PID: %d\n", cmd.Process.Pid)
// 立即通知前端AI 正在思考(避免用户以为卡死)
callback(ai.StreamChunk{Content: "💭 *正在思考...*\n\n"})
// 前端已有 loading 动画,无需在 content 中注入"正在思考"
// 逐行读取流式 JSON 输出
scanner := bufio.NewScanner(stdout)
@@ -131,14 +130,18 @@ func (p *ClaudeCLIProvider) ChatStream(ctx context.Context, req ai.ChatRequest,
// 助手消息开始或文本内容
if event.Message.Content != nil {
for _, block := range event.Message.Content {
if block.Type == "text" && block.Text != "" {
if block.Type == "thinking" && block.Thinking != "" {
callback(ai.StreamChunk{Thinking: block.Thinking})
} else if block.Type == "text" && block.Text != "" {
callback(ai.StreamChunk{Content: block.Text})
}
}
}
case "content_block_delta":
// 增量文本
if event.Delta.Text != "" {
// 增量文本或增量思考
if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
callback(ai.StreamChunk{Thinking: event.Delta.Thinking})
} else if event.Delta.Text != "" {
callback(ai.StreamChunk{Content: event.Delta.Text})
}
case "result":
@@ -213,12 +216,15 @@ type cliStreamEvent struct {
Type string `json:"type"`
Message struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
Type string `json:"type"`
Text string `json:"text"`
Thinking string `json:"thinking"`
} `json:"content"`
} `json:"message,omitempty"`
Delta struct {
Text string `json:"text"`
Type string `json:"type"`
Text string `json:"text"`
Thinking string `json:"thinking"`
} `json:"delta,omitempty"`
Result string `json:"result,omitempty"`
Error struct {

View File

@@ -83,7 +83,13 @@ type geminiContent struct {
}
type geminiPart struct {
Text string `json:"text"`
Text string `json:"text,omitempty"`
InlineData *geminiBlob `json:"inlineData,omitempty"`
}
type geminiBlob struct {
MimeType string `json:"mimeType"`
Data string `json:"data"`
}
type geminiGenConfig struct {
@@ -205,10 +211,6 @@ func (p *GeminiProvider) buildRequest(req ai.ChatRequest) geminiRequest {
if temperature <= 0 {
temperature = p.config.Temperature
}
maxTokens := req.MaxTokens
if maxTokens <= 0 {
maxTokens = p.config.MaxTokens
}
var systemInstruction *geminiContent
var contents []geminiContent
@@ -224,9 +226,29 @@ func (p *GeminiProvider) buildRequest(req ai.ChatRequest) geminiRequest {
if role == "assistant" {
role = "model"
}
var parts []geminiPart
text := m.Content
if text == "" && len(m.Images) > 0 {
text = "请描述和分析这张图片。" // 同样避免 Gemini 认为意图不明确
}
if text != "" {
parts = append(parts, geminiPart{Text: text})
}
for _, img := range m.Images {
mimeType, rawBase64, err := ParseDataURI(img)
if err == nil {
parts = append(parts, geminiPart{
InlineData: &geminiBlob{
MimeType: mimeType,
Data: rawBase64,
},
})
}
}
contents = append(contents, geminiContent{
Role: role,
Parts: []geminiPart{{Text: m.Content}},
Parts: parts,
})
}
@@ -235,7 +257,6 @@ func (p *GeminiProvider) buildRequest(req ai.ChatRequest) geminiRequest {
SystemInstruction: systemInstruction,
GenerationConfig: geminiGenConfig{
Temperature: temperature,
MaxOutputTokens: maxTokens,
},
}
}
@@ -252,6 +273,12 @@ func (p *GeminiProvider) doRequest(ctx context.Context, url string, body interfa
}
httpReq.Header.Set("Content-Type", "application/json")
if strings.Contains(url, "alt=sse") {
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Connection", "keep-alive")
}
resp, err := p.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("发送请求到 Gemini 失败: %w", err)

View File

@@ -0,0 +1,26 @@
package provider
import (
"fmt"
"strings"
)
// ParseDataURI 解析前端传递的 Data URI返回 mimeType 和去掉前缀的 rawBase64
func ParseDataURI(dataURI string) (mimeType, rawBase64 string, err error) {
if !strings.HasPrefix(dataURI, "data:") {
// 如果前端漏了前缀,默认容错当做 jpeg 处理
return "image/jpeg", dataURI, nil
}
parts := strings.SplitN(dataURI, ",", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid data URI format")
}
meta := strings.TrimPrefix(parts[0], "data:")
metaParts := strings.Split(meta, ";")
mimeType = metaParts[0]
if mimeType == "" {
mimeType = "image/jpeg" // fallback
}
rawBase64 = parts[1]
return mimeType, rawBase64, nil
}

View File

@@ -88,18 +88,67 @@ type openAIChatRequest struct {
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
Tools []ai.Tool `json:"tools,omitempty"`
}
type openAIChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Role string `json:"role"`
Content interface{} `json:"content,omitempty"`
ToolCalls []ai.ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
func buildOpenAIMessages(reqMessages []ai.Message, modelName string, baseURL string) []openAIChatMessage {
messages := make([]openAIChatMessage, len(reqMessages))
for i, m := range reqMessages {
if m.Role == "tool" {
messages[i] = openAIChatMessage{Role: m.Role, Content: m.Content, ToolCallID: m.ToolCallID}
continue
}
if len(m.ToolCalls) > 0 {
messages[i] = openAIChatMessage{Role: m.Role, Content: m.Content, ToolCalls: m.ToolCalls}
continue
}
if len(m.Images) > 0 {
var contentParts []map[string]interface{}
text := m.Content
if text == "" {
text = "请描述和分析这张图片。" // 兼容部分模型(如 ZhipuAI/GLM-4V强制要求图片必须伴随有效文本块同时防止强 System Prompt 下模型当成空消息处理
}
contentParts = append(contentParts, map[string]interface{}{
"type": "text",
"text": text,
})
for _, img := range m.Images {
imgURL := img
// 仅当直接请求智谱官方 API 域名时(它原生不接受 data 协议前缀),才截取裸 Base64
if strings.Contains(strings.ToLower(baseURL), "bigmodel") {
if _, raw, err := ParseDataURI(img); err == nil {
imgURL = raw
}
}
contentParts = append(contentParts, map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{
"url": imgURL,
},
})
}
messages[i] = openAIChatMessage{Role: m.Role, Content: contentParts}
} else {
messages[i] = openAIChatMessage{Role: m.Role, Content: m.Content}
}
}
return messages
}
// openAIChatResponse OpenAI API 响应体
type openAIChatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
Content string `json:"content"`
ToolCalls []ai.ToolCall `json:"tool_calls,omitempty"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
@@ -114,10 +163,22 @@ type openAIChatResponse struct {
}
// openAIStreamChunk SSE 流式响应片段
type openAIToolCallDelta struct {
Index int `json:"index"`
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Function *struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
} `json:"function,omitempty"`
}
type openAIStreamChunk struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
ToolCalls []openAIToolCallDelta `json:"tool_calls,omitempty"`
} `json:"delta"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
@@ -131,26 +192,19 @@ func (p *OpenAIProvider) Chat(ctx context.Context, req ai.ChatRequest) (*ai.Chat
return nil, err
}
messages := make([]openAIChatMessage, len(req.Messages))
for i, m := range req.Messages {
messages[i] = openAIChatMessage{Role: m.Role, Content: m.Content}
}
messages := buildOpenAIMessages(req.Messages, p.config.Model, p.baseURL)
temperature := req.Temperature
if temperature <= 0 {
temperature = p.config.Temperature
}
maxTokens := req.MaxTokens
if maxTokens <= 0 {
maxTokens = p.config.MaxTokens
}
body := openAIChatRequest{
Model: p.config.Model,
Messages: messages,
Temperature: temperature,
MaxTokens: maxTokens,
Stream: false,
Tools: req.Tools,
}
respBody, err := p.doRequest(ctx, body)
@@ -177,6 +231,7 @@ func (p *OpenAIProvider) Chat(ctx context.Context, req ai.ChatRequest) (*ai.Chat
CompletionTokens: result.Usage.CompletionTokens,
TotalTokens: result.Usage.TotalTokens,
},
ToolCalls: result.Choices[0].Message.ToolCalls,
}, nil
}
@@ -185,26 +240,19 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ai.ChatRequest, cal
return err
}
messages := make([]openAIChatMessage, len(req.Messages))
for i, m := range req.Messages {
messages[i] = openAIChatMessage{Role: m.Role, Content: m.Content}
}
messages := buildOpenAIMessages(req.Messages, p.config.Model, p.baseURL)
temperature := req.Temperature
if temperature <= 0 {
temperature = p.config.Temperature
}
maxTokens := req.MaxTokens
if maxTokens <= 0 {
maxTokens = p.config.MaxTokens
}
body := openAIChatRequest{
Model: p.config.Model,
Messages: messages,
Temperature: temperature,
MaxTokens: maxTokens,
Stream: true,
Tools: req.Tools,
}
respBody, err := p.doRequest(ctx, body)
@@ -214,6 +262,8 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ai.ChatRequest, cal
defer respBody.Close()
receivedContent := false
var activeToolCalls []ai.ToolCall
scanner := bufio.NewScanner(respBody)
// 增大 scanner buffer防止长行被截断
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
@@ -245,12 +295,49 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ai.ChatRequest, cal
return nil
}
if len(chunk.Choices) > 0 {
content := chunk.Choices[0].Delta.Content
choice := chunk.Choices[0]
// Handle ToolCalls delta
if len(choice.Delta.ToolCalls) > 0 {
receivedContent = true
for _, tcDelta := range choice.Delta.ToolCalls {
// Expand activeToolCalls slice if index is larger
for len(activeToolCalls) <= tcDelta.Index {
activeToolCalls = append(activeToolCalls, ai.ToolCall{Type: "function"})
}
if tcDelta.ID != "" {
activeToolCalls[tcDelta.Index].ID = tcDelta.ID
}
if tcDelta.Function != nil {
if tcDelta.Function.Name != "" {
activeToolCalls[tcDelta.Index].Function.Name += tcDelta.Function.Name
}
if tcDelta.Function.Arguments != "" {
activeToolCalls[tcDelta.Index].Function.Arguments += tcDelta.Function.Arguments
}
}
}
// 实时推送目前已解析的 ToolCalls 状态
callback(ai.StreamChunk{ToolCalls: activeToolCalls})
}
content := choice.Delta.Content
if content != "" {
receivedContent = true
callback(ai.StreamChunk{Content: content})
}
if chunk.Choices[0].FinishReason != nil {
// 支持 DeepSeek/千问等模型的 reasoning_content 字段
if choice.Delta.ReasoningContent != "" {
receivedContent = true
callback(ai.StreamChunk{Thinking: choice.Delta.ReasoningContent})
}
if choice.FinishReason != nil {
if *choice.FinishReason == "tool_calls" {
callback(ai.StreamChunk{ToolCalls: activeToolCalls, Done: true})
return nil
}
callback(ai.StreamChunk{Done: true})
return nil
}
@@ -296,6 +383,13 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, body interface{}) (io.Re
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.config.APIKey)
// 仅在流式请求时明确声明 SSE防止代理缓冲
if strings.Contains(string(jsonBody), `"stream":true`) || strings.Contains(string(jsonBody), `"stream": true`) {
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Connection", "keep-alive")
}
// 自定义 headers用于兼容各类 OpenAI 兼容服务)
for k, v := range p.config.Headers {
httpReq.Header.Set(k, v)

View File

@@ -114,7 +114,7 @@ func (s *Service) AIDeleteProvider(id string) error {
return s.saveConfig()
}
// AITestProvider 测试 Provider 配置是否可用
// AITestProvider 测试 Provider 配置是否可用,仅测试端点连通性与密钥,不实际调用对话
func (s *Service) AITestProvider(config ai.ProviderConfig) map[string]interface{} {
// 如果传入脱敏的 key使用已保存的 key
s.mu.RLock()
@@ -128,30 +128,84 @@ func (s *Service) AITestProvider(config ai.ProviderConfig) map[string]interface{
}
s.mu.RUnlock()
p, err := provider.NewProvider(config)
if err != nil {
return map[string]interface{}{"success": false, "message": err.Error()}
}
if err := p.Validate(); err != nil {
return map[string]interface{}{"success": false, "message": err.Error()}
baseURL := strings.TrimRight(strings.TrimSpace(config.BaseURL), "/")
providerType := config.Type
if providerType == "custom" && config.APIFormat != "" {
providerType = config.APIFormat
}
ctx, cancel := context.WithTimeout(context.Background(), 30*1000*1000*1000) // 30s
defer cancel()
client := &http.Client{Timeout: 10 * time.Second}
var err error
switch providerType {
case "openai":
if baseURL == "" {
baseURL = "https://api.openai.com/v1"
}
if !strings.HasSuffix(baseURL, "/v1") && !strings.Contains(baseURL, "/v1/") {
baseURL = baseURL + "/v1"
}
// 使用 /models 端点验证连通性和鉴权
req, _ := http.NewRequest("GET", baseURL+"/models", nil)
req.Header.Set("Authorization", "Bearer "+config.APIKey)
for k, v := range config.Headers {
req.Header.Set(k, v)
}
resp, reqErr := client.Do(req)
if reqErr != nil {
err = reqErr
} else {
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
err = fmt.Errorf("API Key 验证失败 (HTTP %d)", resp.StatusCode)
} else if resp.StatusCode >= 500 {
err = fmt.Errorf("上游服务器内部错误 (HTTP %d)", resp.StatusCode)
}
}
case "anthropic":
if baseURL == "" {
baseURL = "https://api.anthropic.com"
}
req, _ := http.NewRequest("GET", baseURL, nil)
resp, reqErr := client.Do(req)
if reqErr != nil {
err = reqErr
} else {
resp.Body.Close()
}
case "gemini":
if baseURL == "" {
baseURL = "https://generativelanguage.googleapis.com"
}
req, _ := http.NewRequest("GET", baseURL+"/v1beta/models?key="+config.APIKey, nil)
resp, reqErr := client.Do(req)
if reqErr != nil {
err = reqErr
} else {
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest {
err = fmt.Errorf("API Key 无效或请求错误 (HTTP %d)", resp.StatusCode)
}
}
default:
if baseURL != "" {
req, _ := http.NewRequest("GET", baseURL, nil)
resp, reqErr := client.Do(req)
if reqErr != nil {
err = reqErr
} else {
resp.Body.Close()
}
}
}
resp, err := p.Chat(ctx, ai.ChatRequest{
Messages: []ai.Message{
{Role: "user", Content: "Hi, please respond with 'OK' to confirm the connection is working."},
},
MaxTokens: 10,
})
if err != nil {
return map[string]interface{}{"success": false, "message": fmt.Sprintf("连接测试失败: %s", err.Error())}
}
return map[string]interface{}{
"success": true,
"message": fmt.Sprintf("连接成功!模型响应: %s", truncateString(resp.Content, 100)),
"message": "端点连通性测试成功!",
}
}
@@ -364,19 +418,14 @@ func (s *Service) AISetContextLevel(level string) {
// --- AI 对话 ---
// AIChatSend 同步发送 AI 对话(非流式)
func (s *Service) AIChatSend(messages []map[string]string) map[string]interface{} {
// AIChatSend 非流式发送 AI 对话
func (s *Service) AIChatSend(messages []ai.Message, tools []ai.Tool) map[string]interface{} {
p, err := s.getActiveProvider()
if err != nil {
return map[string]interface{}{"success": false, "error": err.Error()}
}
var aiMessages []ai.Message
for _, m := range messages {
aiMessages = append(aiMessages, ai.Message{Role: m["role"], Content: m["content"]})
}
resp, err := p.Chat(context.Background(), ai.ChatRequest{Messages: aiMessages})
resp, err := p.Chat(context.Background(), ai.ChatRequest{Messages: messages, Tools: tools})
if err != nil {
return map[string]interface{}{"success": false, "error": err.Error()}
}
@@ -384,6 +433,7 @@ func (s *Service) AIChatSend(messages []map[string]string) map[string]interface{
return map[string]interface{}{
"success": true,
"content": resp.Content,
"tool_calls": resp.ToolCalls,
"tokensUsed": map[string]int{
"promptTokens": resp.TokensUsed.PromptTokens,
"completionTokens": resp.TokensUsed.CompletionTokens,
@@ -393,7 +443,7 @@ func (s *Service) AIChatSend(messages []map[string]string) map[string]interface{
}
// AIChatStream 流式发送 AI 对话(通过 EventsEmit 推送)
func (s *Service) AIChatStream(sessionID string, messages []map[string]string) {
func (s *Service) AIChatStream(sessionID string, messages []ai.Message, tools []ai.Tool) {
streamCtx, cancel := context.WithCancel(context.Background())
s.mu.Lock()
s.cancelFuncs[sessionID] = cancel
@@ -416,16 +466,13 @@ func (s *Service) AIChatStream(sessionID string, messages []map[string]string) {
return
}
var aiMessages []ai.Message
for _, m := range messages {
aiMessages = append(aiMessages, ai.Message{Role: m["role"], Content: m["content"]})
}
err = p.ChatStream(streamCtx, ai.ChatRequest{Messages: aiMessages}, func(chunk ai.StreamChunk) {
err = p.ChatStream(streamCtx, ai.ChatRequest{Messages: messages, Tools: tools}, func(chunk ai.StreamChunk) {
wailsRuntime.EventsEmit(s.ctx, "ai:stream:"+sessionID, map[string]interface{}{
"content": chunk.Content,
"done": chunk.Done,
"error": chunk.Error,
"content": chunk.Content,
"thinking": chunk.Thinking,
"tool_calls": chunk.ToolCalls,
"done": chunk.Done,
"error": chunk.Error,
})
})

View File

@@ -1,9 +1,35 @@
package ai
// ToolCall 表示 AI 发出的工具调用
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"` // "function"
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
// ToolFunction 表示可使用的函数定义
type ToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters any `json:"parameters"` // JSON Schema definitions
}
// Tool 工具申明
type Tool struct {
Type string `json:"type"` // "function"
Function ToolFunction `json:"function"`
}
// Message 表示一条对话消息
type Message struct {
Role string `json:"role"` // "system" | "user" | "assistant"
Content string `json:"content"`
Role string `json:"role"` // "system" | "user" | "assistant" | "tool"
Content string `json:"content"`
Images []string `json:"images,omitempty"` // base64 encoded images with data:image/png;base64,... prefix
ToolCallID string `json:"tool_call_id,omitempty"` // 当 role 为 "tool" 时必须传递
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // 当 role 为 "assistant" 并试图调工具时传递
}
// ChatRequest AI 对话请求
@@ -11,12 +37,14 @@ type ChatRequest struct {
Messages []Message `json:"messages"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"maxTokens"`
Tools []Tool `json:"tools,omitempty"`
}
// ChatResponse AI 对话响应
type ChatResponse struct {
Content string `json:"content"`
TokensUsed TokenUsage `json:"tokensUsed"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
// TokenUsage token 用量统计
@@ -28,9 +56,11 @@ type TokenUsage struct {
// StreamChunk 流式响应片段
type StreamChunk struct {
Content string `json:"content"`
Done bool `json:"done"`
Error string `json:"error,omitempty"`
Content string `json:"content"`
Thinking string `json:"thinking,omitempty"`
Done bool `json:"done"`
Error string `json:"error,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
// ProviderConfig AI Provider 配置