feat: add admin publishing workflow and yar theme

Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
This commit is contained in:
yarnom 2026-06-01 15:48:04 +08:00
parent b78f4b39c9
commit f0b50d13ea
121 changed files with 27139 additions and 550 deletions

View file

@ -24,6 +24,14 @@ type Config struct {
Model string
}
type LocalConfig struct {
URL string
Model string
Temperature float64
TopP float64
NumPredict int
}
type deepSeekMessage struct {
Role string `json:"role"`
Content string `json:"content"`
@ -58,6 +66,24 @@ type slugResponse struct {
Alternatives []string `json:"alternatives"`
}
type localGenerateRequest struct {
Model string `json:"model"`
Stream bool `json:"stream"`
Options localOptions `json:"options"`
Prompt string `json:"prompt"`
}
type localOptions struct {
Temperature float64 `json:"temperature"`
TopP float64 `json:"top_p"`
NumPredict int `json:"num_predict"`
}
type localGenerateResponse struct {
Response string `json:"response"`
Error string `json:"error,omitempty"`
}
func GenerateSlug(ctx context.Context, config Config, title string, summary string) (string, error) {
apiKey := strings.TrimSpace(config.APIKey)
if apiKey == "" {
@ -168,6 +194,85 @@ JSON format: {"slug":"example-slug","alternatives":["another-slug"]}`,
return slug, nil
}
func GenerateLocalSlug(ctx context.Context, config LocalConfig, title string, summary string) (string, error) {
url := strings.TrimSpace(config.URL)
if url == "" {
return "", errors.New("local LLM URL is empty")
}
model := strings.TrimSpace(config.Model)
if model == "" {
return "", errors.New("local LLM model is empty")
}
temperature := config.Temperature
if temperature == 0 {
temperature = 0.1
}
topP := config.TopP
if topP == 0 {
topP = 0.8
}
numPredict := config.NumPredict
if numPredict == 0 {
numPredict = 32
}
prompt := fmt.Sprintf(`Convert the following Chinese blog title into a concise English URL slug.
Output only the slug. Lowercase only. Use hyphens. Max 8 words.
Title: %s`, title)
if strings.TrimSpace(summary) != "" {
prompt += "\nSummary: " + summary
}
payload := localGenerateRequest{
Model: model,
Stream: false,
Options: localOptions{
Temperature: temperature,
TopP: topP,
NumPredict: numPredict,
},
Prompt: prompt,
}
body, err := json.Marshal(payload)
if err != nil {
return "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return "", err
}
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("local LLM returned %s: %s", resp.Status, strings.TrimSpace(string(respBody)))
}
var generated localGenerateResponse
if err := json.Unmarshal(respBody, &generated); err != nil {
return "", err
}
if generated.Error != "" {
return "", fmt.Errorf("local LLM error: %s", generated.Error)
}
slug := sanitizeSlug(generated.Response)
if slug == "" {
return "", errors.New("local LLM returned an empty slug")
}
return slug, nil
}
func sanitizeSlug(slug string) string {
slug = strings.ToLower(strings.TrimSpace(slug))
re := regexp.MustCompile(`[^a-z0-9]+`)