cef681ec87
Nuevo sistema de generacion automatica de avatares: - pkg/avatar/ — tipos puros y URL builders para proveedores gratuitos: DiceBear (bottts, pixel-art, etc.), RoboHash (robots, monsters), Multiavatar (multicultural). Sin I/O. - shell/avatar/ — fetcher impuro: descarga imagen por HTTP a temp file. - agentctl auto-avatar <id> — genera, descarga, sube y activa avatar con un solo comando. Soporta --provider, --style, --set, --dry-run. Respeta pure core / impure shell. El seed del avatar es el agent ID, asi cada bot obtiene una imagen unica y determinista. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
131 lines
3.3 KiB
Go
131 lines
3.3 KiB
Go
// Package avatar fetches avatar images from external providers.
|
|
// Impure shell: performs HTTP I/O.
|
|
package avatar
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/enmanuel/agents/pkg/avatar"
|
|
)
|
|
|
|
const (
|
|
fetchTimeout = 30 * time.Second
|
|
maxBytes = 10 * 1024 * 1024 // 10 MB
|
|
)
|
|
|
|
// Fetch downloads an avatar image from the given provider using seed as the
|
|
// generation key (typically the agent ID or display name). Returns the path
|
|
// to a temporary file containing the image.
|
|
// The caller is responsible for removing the temp file when done.
|
|
func Fetch(ctx context.Context, provider avatar.Provider, seed string, opts avatar.Options) (string, error) {
|
|
imageURL := avatar.URL(provider, seed, opts)
|
|
return Download(ctx, imageURL)
|
|
}
|
|
|
|
// Download fetches the image at imageURL and writes it to a temp file.
|
|
// Returns the temp file path. The caller must remove the file when done.
|
|
func Download(ctx context.Context, imageURL string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, fetchTimeout)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("User-Agent", "agents-avatar-fetcher/1.0")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("fetch %s: %w", imageURL, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("fetch %s: HTTP %d", imageURL, resp.StatusCode)
|
|
}
|
|
|
|
ext := extensionFromContentType(resp.Header.Get("Content-Type"))
|
|
|
|
tmp, err := os.CreateTemp("", "avatar-*"+ext)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
|
|
if _, err := io.Copy(tmp, io.LimitReader(resp.Body, maxBytes)); err != nil {
|
|
tmp.Close()
|
|
os.Remove(tmp.Name())
|
|
return "", fmt.Errorf("write temp file: %w", err)
|
|
}
|
|
|
|
if err := tmp.Close(); err != nil {
|
|
os.Remove(tmp.Name())
|
|
return "", fmt.Errorf("close temp file: %w", err)
|
|
}
|
|
|
|
return tmp.Name(), nil
|
|
}
|
|
|
|
// FetchToDir downloads the avatar and saves it to dir/<seed>.<ext>.
|
|
// Creates dir if it doesn't exist. Returns the final file path.
|
|
func FetchToDir(ctx context.Context, provider avatar.Provider, seed string, opts avatar.Options, dir string) (string, error) {
|
|
tmpPath, err := Fetch(ctx, provider, seed, opts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(tmpPath)
|
|
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return "", fmt.Errorf("create dir %s: %w", dir, err)
|
|
}
|
|
|
|
ext := filepath.Ext(tmpPath)
|
|
finalPath := filepath.Join(dir, seed+ext)
|
|
|
|
src, err := os.Open(tmpPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open temp: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.Create(finalPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create %s: %w", finalPath, err)
|
|
}
|
|
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
dst.Close()
|
|
os.Remove(finalPath)
|
|
return "", fmt.Errorf("copy to %s: %w", finalPath, err)
|
|
}
|
|
|
|
if err := dst.Close(); err != nil {
|
|
os.Remove(finalPath)
|
|
return "", err
|
|
}
|
|
|
|
return finalPath, nil
|
|
}
|
|
|
|
func extensionFromContentType(ct string) string {
|
|
switch ct {
|
|
case "image/png":
|
|
return ".png"
|
|
case "image/jpeg":
|
|
return ".jpg"
|
|
case "image/svg+xml":
|
|
return ".svg"
|
|
case "image/webp":
|
|
return ".webp"
|
|
case "image/gif":
|
|
return ".gif"
|
|
default:
|
|
return ".png"
|
|
}
|
|
}
|