✨Feat: 构建对话终端

This commit is contained in:
2026-03-20 14:26:24 +08:00
parent 3f5c0bbae1
commit 355645efdf
17 changed files with 1338 additions and 2 deletions
+155
View File
@@ -0,0 +1,155 @@
package ai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// SiliconFlowClient 硅基流动API客户端
type SiliconFlowClient struct {
apiKey string
baseURL string
model string
timeout time.Duration
maxRetries int
}
// Config AI配置
type Config struct {
Endpoint string `yaml:"endpoint"`
Model string `yaml:"model"`
Timeout int `yaml:"timeout"`
MaxRetries int `yaml:"max_retries"`
ApiKey string `yaml:"api_key"`
}
// ChatMessage 聊天消息
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
// ChatRequest 聊天请求
type ChatRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
Temperature float64 `json:"temperature"`
Stream bool `json:"stream"`
}
// ChatResponse 聊天响应
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage Usage `json:"usage"`
}
// Choice 选择项
type Choice struct {
Index int `json:"index"`
Message ChatMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
// Usage 使用情况
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// NewClient 创建新的AI客户端
func NewClient(config Config) *SiliconFlowClient {
return &SiliconFlowClient{
apiKey: config.ApiKey,
baseURL: config.Endpoint,
model: config.Model,
timeout: time.Duration(config.Timeout) * time.Second,
maxRetries: config.MaxRetries,
}
}
// Generate 生成内容
func (c *SiliconFlowClient) Generate(prompt string) (string, error) {
reqBody := ChatRequest{
Model: c.model,
Messages: []ChatMessage{
{
Role: "user",
Content: prompt,
},
},
Temperature: 0.7,
Stream: false,
}
reqJSON, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
var lastErr error
for attempt := 0; attempt < c.maxRetries; attempt++ {
result, err := c.sendRequest(reqJSON)
if err == nil {
return result, nil
}
lastErr = err
if attempt < c.maxRetries-1 {
time.Sleep(time.Duration(attempt+1) * time.Second)
}
}
return "", fmt.Errorf("failed after %d attempts: %w", c.maxRetries, lastErr)
}
func (c *SiliconFlowClient) sendRequest(reqJSON []byte) (string, error) {
url := c.baseURL + "/chat/completions"
req, err := http.NewRequest("POST", url, bytes.NewReader(reqJSON))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: c.timeout,
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API returned status code %d: %s", resp.StatusCode, string(body))
}
var chatResp ChatResponse
if err := json.Unmarshal(body, &chatResp); err != nil {
return "", fmt.Errorf("failed to unmarshal response: %w", err)
}
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("no choices in response")
}
return chatResp.Choices[0].Message.Content, nil
}
+63
View File
@@ -0,0 +1,63 @@
package generators
import (
"learning-assistant/internal/ai"
)
// GeneratorType 生成器类型
type GeneratorType string
const (
GeneratorMermaid GeneratorType = "mermaid"
GeneratorGuodegang GeneratorType = "guodegang"
GeneratorQuiz GeneratorType = "quiz"
GeneratorCard GeneratorType = "card"
GeneratorScenario GeneratorType = "scenario"
)
// GenerateResult 生成结果
type GenerateResult struct {
Type GeneratorType
Content string
Error error
}
// BaseGenerator 基础生成器
type BaseGenerator struct {
client *ai.SiliconFlowClient
promptManager *ai.PromptManager
}
// NewBaseGenerator 创建基础生成器
func NewBaseGenerator(client *ai.SiliconFlowClient, promptManager *ai.PromptManager) *BaseGenerator {
return &BaseGenerator{
client: client,
promptManager: promptManager,
}
}
// Generate 生成内容
func (g *BaseGenerator) Generate(genType GeneratorType, knowledge string, resultChan chan<- GenerateResult) {
prompt, err := g.promptManager.GeneratePrompt(string(genType), knowledge)
if err != nil {
resultChan <- GenerateResult{
Type: genType,
Error: err,
}
return
}
content, err := g.client.Generate(prompt)
if err != nil {
resultChan <- GenerateResult{
Type: genType,
Error: err,
}
return
}
resultChan <- GenerateResult{
Type: genType,
Content: content,
}
}
+71
View File
@@ -0,0 +1,71 @@
package ai
import (
"embed"
"fmt"
"strings"
"text/template"
)
//go:embed prompts/*.txt
var promptFS embed.FS
// PromptManager 提示词管理器
type PromptManager struct {
templates map[string]*template.Template
}
// NewPromptManager 创建提示词管理器
func NewPromptManager() *PromptManager {
pm := &PromptManager{
templates: make(map[string]*template.Template),
}
pm.loadTemplates()
return pm
}
func (pm *PromptManager) loadTemplates() {
templateFiles := map[string]string{
"mermaid": "prompts/mermaid.txt",
"guodegang": "prompts/guodegang.txt",
"quiz": "prompts/quiz.txt",
"card": "prompts/card.txt",
"scenario": "prompts/scenario.txt",
}
for name, path := range templateFiles {
content, err := promptFS.ReadFile(path)
if err != nil {
continue
}
tmpl, err := template.New(name).Parse(string(content))
if err != nil {
continue
}
pm.templates[name] = tmpl
}
}
// GeneratePrompt 生成提示词
func (pm *PromptManager) GeneratePrompt(templateType string, knowledge string) (string, error) {
tmpl, ok := pm.templates[templateType]
if !ok {
return "", fmt.Errorf("template type %s not found", templateType)
}
var buf strings.Builder
data := struct {
Knowledge string
}{
Knowledge: knowledge,
}
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to execute template: %w", err)
}
return buf.String(), nil
}
+23
View File
@@ -0,0 +1,23 @@
为"{{.Knowledge}}"制作知识记忆卡片,帮助用户快速掌握关键知识点,要求:
1. 结构清晰,重点突出
2. 使用emoji图标增强可读性
3. 每个部分简洁明了,方便记忆
输出格式:
📌 核心概念:[精确的知识点定义,50字以内]
🔑 关键要点:
- 要点1(具体说明,20字以内)
- 要点2(具体说明,20字以内)
- 要点3(具体说明,20字以内)
- 要点4(具体说明,20字以内)
⚠️ 常见误区:
- 误区1:[说明]
- 误区2:[说明]
✅ 最佳实践:
- 实践1:[具体建议]
- 实践2:[具体建议]
🎯 记忆口诀:[朗朗上口的口诀,帮助记忆]
+12
View File
@@ -0,0 +1,12 @@
用北京话、郭德纲相声演员的口吻讲解"{{.Knowledge}}",要求:
1. 开头用"咳咳,咱今儿个来说说这个{{.Knowledge}}..."作为开场
2. 中间穿插一些相声评书风格的生动比喻和例子
3. 语气轻松诙谐、接地气,但内容要准确专业
4. 偶尔使用相声常用语,比如"您听好了"、"这就好比"、"想当年"、"您猜怎么着"
5. 每讲完一个要点,可以用"这话怎么说呢?"来过渡
6. 最后用"您记住了吗?下次要是有人问起来,您就把这么一通摆出来,准保让他对您刮目相看!"收尾
注意:
- 不要过度夸张,保持内容的准确性和专业性
- 语言要通俗易懂,避免过于晦涩的专业术语
- 字数控制在300-500字
+19
View File
@@ -0,0 +1,19 @@
请为"{{.Knowledge}}"生成一个Mermaid思维导图或流程图,要求:
1. 使用graph TD或flowchart TD格式
2. 清晰展示知识点的结构层次和关系
3. 关键节点使用中文标签
4. 适当使用不同的节点样式区分概念类型
5. 保持代码简洁,便于复制使用
输出格式(只输出代码,不要其他说明文字):
```mermaid
graph TD
A[{{.Knowledge}}] --> B(核心概念1)
A --> C(核心概念2)
B --> B1(子概念1.1)
B --> B2(子概念1.2)
C --> C1(子概念2.1)
style A fill:#f9f,stroke:#333,stroke-width:4px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#bbf,stroke:#333,stroke-width:2px
```
+22
View File
@@ -0,0 +1,22 @@
关于"{{.Knowledge}}",生成10道单选题,要求:
1. 题目类型包括:概念理解、原理分析、应用场景、最佳实践等
2. 难度分布:3道基础题、4道中级题、3道进阶题
3. 每道题都要提供详细的解析说明
4. 选项之间要有明显的区别,避免模棱两可
必须严格按照以下JSON格式输出(只输出JSON,不要其他说明文字):
{
"questions": [
{
"question": "题目文字,准确描述问题",
"options": [
"A. 选项内容",
"B. 选项内容",
"C. 选项内容",
"D. 选项内容"
],
"answer": "A",
"explanation": "详细解析为什么选这个答案,以及为什么其他选项不对"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
为"{{.Knowledge}}"提供3个实际的工程应用场景,要求:
1. 每个场景都要具体、真实、有代表性
2. 包含完整的问题背景、解决方案、代码示例
3. 代码使用伪代码或简化的实现,注重逻辑清晰
4. 说明使用该知识点解决了什么核心问题
5. 强调注意事项和最佳实践
输出格式:
场景1:[场景名称,如"高并发缓存更新"]
背景:
[描述实际业务场景和遇到的问题]
方案:
[说明如何使用{{.Knowledge}}解决问题,分步骤说明]
代码示例:
[使用伪代码或简化代码展示实现方式]
注意事项:
- 关键点1
- 关键点2
场景2:[场景名称]
背景:
...
方案:
...
代码示例:
...
注意事项:
- ...
场景3:[场景名称]
背景:
...
方案:
...
代码示例:
...
注意事项:
- ...