package ai import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "regexp" "strings" "time" ) const ( DefaultDeepSeekBaseURL = "https://api.deepseek.com" DefaultDeepSeekModel = "deepseek-v4-pro" ) type Config struct { APIKey string BaseURL string Model string } type deepSeekMessage struct { Role string `json:"role"` Content string `json:"content"` } type deepSeekChatRequest struct { Model string `json:"model"` Messages []deepSeekMessage `json:"messages"` Thinking map[string]string `json:"thinking,omitempty"` ResponseFormat map[string]string `json:"response_format,omitempty"` Temperature float64 `json:"temperature"` MaxTokens int `json:"max_tokens"` } type deepSeekChatResponse struct { Choices []struct { FinishReason string `json:"finish_reason"` Message struct { Role string `json:"role"` Content string `json:"content"` ReasoningContent string `json:"reasoning_content"` } `json:"message"` } `json:"choices"` Error *struct { Message string `json:"message"` Type string `json:"type"` } `json:"error,omitempty"` } type slugResponse struct { Slug string `json:"slug"` Alternatives []string `json:"alternatives"` } func GenerateSlug(ctx context.Context, config Config, title string, summary string) (string, error) { apiKey := strings.TrimSpace(config.APIKey) if apiKey == "" { return "", errors.New("DeepSeek API key is empty") } baseURL := strings.TrimRight(strings.TrimSpace(config.BaseURL), "/") if baseURL == "" { baseURL = DefaultDeepSeekBaseURL } model := strings.TrimSpace(config.Model) if model == "" { model = DefaultDeepSeekModel } userPrompt := fmt.Sprintf("Title: %s\nSummary: %s", title, summary) payload := deepSeekChatRequest{ Model: model, Messages: []deepSeekMessage{ { Role: "system", Content: `You generate concise English URL slugs for blog posts. Return JSON only. Rules: - lowercase English - use hyphen separators - only a-z, 0-9, and hyphen - translate non-English titles by meaning into natural English - do not use pinyin, transliteration, romanization, or pronunciation-based spellings - no leading or trailing hyphen - max 80 characters - preserve important technical terms - do not include explanations Examples: - title "喜欢你" -> {"slug":"like-you","alternatives":["i-like-you","loving-you"]} - title "用 Go 和 Astro 构建个人博客" -> {"slug":"building-a-personal-blog-with-go-and-astro","alternatives":["go-astro-personal-blog"]} JSON format: {"slug":"example-slug","alternatives":["another-slug"]}`, }, {Role: "user", Content: userPrompt}, }, Thinking: map[string]string{"type": "disabled"}, ResponseFormat: map[string]string{"type": "json_object"}, Temperature: 0.2, MaxTokens: 300, } body, err := json.Marshal(payload) if err != nil { return "", err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return "", err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("DeepSeek API returned %s: %s", resp.Status, strings.TrimSpace(string(respBody))) } var chatResp deepSeekChatResponse if err := json.Unmarshal(respBody, &chatResp); err != nil { return "", err } if chatResp.Error != nil { return "", fmt.Errorf("DeepSeek API error: %s", chatResp.Error.Message) } if len(chatResp.Choices) == 0 { return "", errors.New("DeepSeek API returned no choices") } choice := chatResp.Choices[0] content := strings.TrimSpace(choice.Message.Content) if content == "" { return "", fmt.Errorf("DeepSeek returned empty content, finish_reason=%q", choice.FinishReason) } var slugResp slugResponse if err := json.Unmarshal([]byte(content), &slugResp); err != nil { return "", fmt.Errorf("failed to parse DeepSeek slug JSON: %w", err) } slug := sanitizeSlug(slugResp.Slug) if slug == "" { for _, alternative := range slugResp.Alternatives { slug = sanitizeSlug(alternative) if slug != "" { break } } } if slug == "" { return "", errors.New("DeepSeek returned an empty slug") } return slug, nil } func sanitizeSlug(slug string) string { slug = strings.ToLower(strings.TrimSpace(slug)) re := regexp.MustCompile(`[^a-z0-9]+`) slug = re.ReplaceAllString(slug, "-") slug = strings.Trim(slug, "-") for strings.Contains(slug, "--") { slug = strings.ReplaceAll(slug, "--", "-") } if len(slug) > 80 { slug = strings.Trim(slug[:80], "-") } return slug }