Initialize blog scaffold

Add the CLI, site, and sample content so the project can run locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yarnom 2026-05-28 16:58:30 +08:00
parent 9d2628b318
commit b78f4b39c9
40 changed files with 9140 additions and 0 deletions

View file

@ -0,0 +1,183 @@
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
}