154 lines
4.0 KiB
Go
154 lines
4.0 KiB
Go
package bot
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/openai/openai-go"
|
||
"github.com/openai/openai-go/option"
|
||
"github.com/openai/openai-go/shared"
|
||
"myaibot/internal/config"
|
||
)
|
||
|
||
const maxHistory = 20
|
||
|
||
type Bot struct {
|
||
clients map[string]*openai.Client
|
||
cfg *config.Config
|
||
provider *config.Provider
|
||
model string
|
||
history []openai.ChatCompletionMessageParamUnion
|
||
systemPrompt string
|
||
}
|
||
|
||
func New(cfg *config.Config) *Bot {
|
||
b := &Bot{
|
||
clients: make(map[string]*openai.Client),
|
||
cfg: cfg,
|
||
systemPrompt: cfg.SystemPrompt,
|
||
}
|
||
b.provider = config.FindProvider(cfg.DefaultProvider)
|
||
b.model = cfg.DefaultModel
|
||
return b
|
||
}
|
||
|
||
func (b *Bot) client() *openai.Client {
|
||
if c, ok := b.clients[b.provider.Name]; ok {
|
||
return c
|
||
}
|
||
c := openai.NewClient(
|
||
option.WithAPIKey(b.provider.APIKey),
|
||
option.WithBaseURL(b.provider.BaseURL),
|
||
)
|
||
b.clients[b.provider.Name] = &c
|
||
return &c
|
||
}
|
||
|
||
func (b *Bot) Models() []string {
|
||
return config.AllModels()
|
||
}
|
||
|
||
func (b *Bot) Current() (string, string) {
|
||
return b.provider.Name, b.model
|
||
}
|
||
|
||
func (b *Bot) SwitchModel(id string) error {
|
||
p, modelName, err := config.ResolveModel(id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
b.provider = p
|
||
b.model = modelName
|
||
return nil
|
||
}
|
||
|
||
func (b *Bot) SetThinking(v string) error {
|
||
if v != "enabled" && v != "disabled" {
|
||
return fmt.Errorf("无效值: %s(可选 enabled/disabled)", v)
|
||
}
|
||
b.provider.Thinking = v
|
||
return nil
|
||
}
|
||
|
||
func (b *Bot) SetEffort(v string) error {
|
||
if v != "low" && v != "high" && v != "max" {
|
||
return fmt.Errorf("无效值: %s(可选 low/high/max)", v)
|
||
}
|
||
b.provider.ReasoningEffort = v
|
||
return nil
|
||
}
|
||
|
||
func (b *Bot) ThinkingConfig() (string, string) {
|
||
return b.provider.Thinking, b.provider.ReasoningEffort
|
||
}
|
||
|
||
func (b *Bot) ContextDump() string {
|
||
var sb strings.Builder
|
||
sb.WriteString("[系统] " + b.systemPrompt + "\n")
|
||
for i, msg := range b.history {
|
||
role, content := "", ""
|
||
switch {
|
||
case msg.OfUser != nil:
|
||
role, content = "用户", msg.OfUser.Content.OfString.Value
|
||
case msg.OfAssistant != nil:
|
||
role, content = "机器人", msg.OfAssistant.Content.OfString.Value
|
||
case msg.OfSystem != nil:
|
||
role, content = "系统", msg.OfSystem.Content.OfString.Value
|
||
}
|
||
if content == "" {
|
||
content = "[多模态内容]"
|
||
}
|
||
sb.WriteString(fmt.Sprintf("[%d] %s: %s\n", i+1, role, content))
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
func (b *Bot) Chat(ctx context.Context, userMsg string, onReasoning, onContent func(string)) (string, error) {
|
||
if b.provider.APIKey == "" {
|
||
return "", fmt.Errorf("供应商 %s 未配置 api_key,请编辑 data/config.yaml", b.provider.Name)
|
||
}
|
||
history := make([]openai.ChatCompletionMessageParamUnion, 0, len(b.history)+2)
|
||
history = append(history, openai.SystemMessage(b.systemPrompt))
|
||
history = append(history, b.history...)
|
||
history = append(history, openai.UserMessage(userMsg))
|
||
|
||
params := openai.ChatCompletionNewParams{
|
||
Model: b.model,
|
||
Messages: history,
|
||
}
|
||
if p := b.provider; p.ReasoningEffort != "" && p.Thinking != "disabled" {
|
||
params.ReasoningEffort = shared.ReasoningEffort(p.ReasoningEffort)
|
||
}
|
||
if b.provider.Thinking != "" {
|
||
params.SetExtraFields(map[string]any{
|
||
"thinking": map[string]string{"type": b.provider.Thinking},
|
||
})
|
||
}
|
||
stream := b.client().Chat.Completions.NewStreaming(ctx, params)
|
||
answer := ""
|
||
for stream.Next() {
|
||
for _, choice := range stream.Current().Choices {
|
||
if rc, ok := choice.Delta.JSON.ExtraFields["reasoning_content"]; ok && rc.Valid() {
|
||
var s string
|
||
if json.Unmarshal([]byte(rc.Raw()), &s) == nil && s != "" {
|
||
onReasoning(s)
|
||
}
|
||
}
|
||
if choice.Delta.Content != "" {
|
||
answer += choice.Delta.Content
|
||
onContent(choice.Delta.Content)
|
||
}
|
||
}
|
||
}
|
||
if err := stream.Err(); err != nil {
|
||
return "", fmt.Errorf("调用 AI 接口失败: %w", err)
|
||
}
|
||
b.history = append(b.history, openai.UserMessage(userMsg), openai.AssistantMessage(answer))
|
||
if len(b.history) > maxHistory {
|
||
b.history = b.history[len(b.history)-maxHistory:]
|
||
}
|
||
return answer, nil
|
||
}
|