From 7b03f54bb542a0bcf3f05a6ccfd31cb8b901743f Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 13 Aug 2026 18:54:34 +0800 Subject: [PATCH] up --- .gitignore | 3 ++ go.mod | 12 +++++ go.sum | 16 ++++++ internal/bot/bot.go | 59 ++++++++++++++++++++++ internal/config/config.go | 100 ++++++++++++++++++++++++++++++++++++++ main.go | 47 +++++++++++++++++- 6 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 go.sum create mode 100644 internal/bot/bot.go create mode 100644 internal/config/config.go diff --git a/.gitignore b/.gitignore index 26c29d1..57cdb91 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ .env.* !.env.example +# Runtime data +/data/ + # Go workspace files go.work go.work.sum diff --git a/go.mod b/go.mod index 2ed91fd..a01c745 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,15 @@ module myaibot go 1.26.4 + +require ( + github.com/openai/openai-go v1.12.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..72014d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= +github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bot/bot.go b/internal/bot/bot.go new file mode 100644 index 0000000..ebe6d8f --- /dev/null +++ b/internal/bot/bot.go @@ -0,0 +1,59 @@ +package bot + +import ( + "context" + "errors" + "fmt" + + "github.com/openai/openai-go" + "github.com/openai/openai-go/option" + "myaibot/internal/config" +) + +const maxHistory = 20 + +type Bot struct { + client *openai.Client + cfg *config.Config + history []openai.ChatCompletionMessageParamUnion +} + +func New(cfg *config.Config) *Bot { + client := openai.NewClient( + option.WithAPIKey(cfg.APIKey), + option.WithBaseURL(cfg.BaseURL), + ) + return &Bot{ + client: &client, + cfg: cfg, + } +} + +func (b *Bot) Chat(ctx context.Context, userMsg string) (string, error) { + if b.cfg.APIKey == "" { + return "", errors.New("未配置 api_key,请编辑 data/config.yaml") + } + history := make([]openai.ChatCompletionMessageParamUnion, 0, len(b.history)+2) + history = append(history, openai.SystemMessage(b.cfg.SystemPrompt)) + history = append(history, b.history...) + history = append(history, openai.UserMessage(userMsg)) + + stream := b.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{ + Model: b.cfg.Model, + Messages: history, + }) + answer := "" + for stream.Next() { + for _, delta := range stream.Current().Choices { + answer += delta.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 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..c8632db --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,100 @@ +package config + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +const ( + configDir = "data" + configFile = "config.yaml" +) + +type Config struct { + BotName string `yaml:"bot_name"` + Port int `yaml:"port"` + LogLevel string `yaml:"log_level"` + APIKey string `yaml:"api_key"` + BaseURL string `yaml:"base_url"` + Model string `yaml:"model"` + SystemPrompt string `yaml:"system_prompt"` +} + +var cfg *Config + +func Load() (*Config, error) { + if cfg != nil { + return cfg, nil + } + if err := os.MkdirAll(configDir, 0o755); err != nil { + return nil, err + } + path := filepath.Join(configDir, configFile) + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := writeDefault(path); err != nil { + return nil, err + } + } + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + cfg = &Config{} + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, err + } + applyDefaults(cfg) + return cfg, nil +} + +func applyDefaults(c *Config) { + def := &Config{ + BotName: "ai-bot", + Port: 8080, + LogLevel: "info", + BaseURL: "https://api.openai.com/v1", + Model: "gpt-4o-mini", + SystemPrompt: "你是一个乐于助人的 AI 助手。", + } + if c.BotName == "" { + c.BotName = def.BotName + } + if c.Port == 0 { + c.Port = def.Port + } + if c.LogLevel == "" { + c.LogLevel = def.LogLevel + } + if c.BaseURL == "" { + c.BaseURL = def.BaseURL + } + if c.Model == "" { + c.Model = def.Model + } + if c.SystemPrompt == "" { + c.SystemPrompt = def.SystemPrompt + } +} + +func GetConfig() *Config { + return cfg +} + +func writeDefault(path string) error { + cfg = &Config{ + BotName: "ai-bot", + Port: 8080, + LogLevel: "info", + APIKey: "", + BaseURL: "https://api.openai.com/v1", + Model: "gpt-4o-mini", + SystemPrompt: "你是一个乐于助人的 AI 助手。", + } + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} diff --git a/main.go b/main.go index a3dd973..0284ca1 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,50 @@ package main -import "fmt" +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "strings" + + "myaibot/internal/bot" + "myaibot/internal/config" +) func main() { - fmt.Println("Hello, World!") + cfg, err := config.Load() + if err != nil { + log.Fatalf("加载配置失败: %v", err) + } + if cfg.APIKey == "" { + log.Println("提示: 未配置 api_key,请编辑 data/config.yaml") + } + fmt.Printf("🤖 %s 已启动 (模型: %s)。输入问题开始对话,输入 /exit 退出。\n", cfg.BotName, cfg.Model) + + b := bot.New(cfg) + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Print("你: ") + if !scanner.Scan() { + break + } + input := strings.TrimSpace(scanner.Text()) + if input == "" { + continue + } + if input == "/exit" || input == "/quit" { + fmt.Println("再见!") + break + } + answer, err := b.Chat(context.Background(), input) + if err != nil { + fmt.Printf("⚠️ %v\n", err) + continue + } + fmt.Printf("%s: %s\n", cfg.BotName, answer) + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } }