All checks were successful
PR Check / check (pull_request) Successful in 57s
Audit corrections (security, concurrency, stability): - chat_engine: bound resp.Choices[0] access, release tool slot per-iteration - conversation_multi: synchronous save under existing lock (was racy fire-and-forget) - workflow/engine: short-circuit on failed deps (no more infinite busy-wait); track failed/skipped status - handlers_workflow: rune-aware truncate for plan goal (UTF-8 safe) - server: CORS limited to localhost origins (was wildcard) - handlers_info / terminal: mask API keys and SSH passwords as "***" in GET responses; preserve stored secret if "***" sent on update - terminal: sshpass uses -e + SSHPASS env var (was both -p and -e) - handlers_chat: MaxBytesReader 50 MB on /api/chat - image_cache: 10 MB cap per image - handlers_config: font size <= 72; profile-save unmarshal errors propagated - handlers_info: /lsp/auto-install ProjectDir restricted to user home - Shell.jsx: parenthesized resize-condition (operator precedence) - orchestrator_test: CleanAIResponse capitalization (fixes failing vet) New features: - platform: detect OS name (Debian, Ubuntu, Windows 11, macOS X.Y) and inject in Studio system prompt next to the date - agents: default timeout 30 min for crush_run/claude_run (cap also 30 min) - agents: new cwd, wsl_distro, wsl_user params; on Windows hosts launch via "wsl -d <distro> -u <user> --cd <cwd> --" - agents: new claude_run tool (mirror of crush_run for Claude Code CLI) - terminal: list installed WSL distros individually in new-tab menu (Windows only) - studio: system prompt rewritten around BMAD-METHOD personas + mandatory delegation template - studio: "Réflexion avancée" toggle — inactive provider produces a preliminary report injected as [RAPPORT PRÉALABLE] context for the active provider - studio: "Historique compressé" toggle — collapses past tool calls to last action only, with "Tout afficher" expansion
107 lines
2.2 KiB
Go
107 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/muyue/muyue/internal/config"
|
|
)
|
|
|
|
var imageDir string
|
|
|
|
func init() {
|
|
dir, err := config.ConfigDir()
|
|
if err != nil {
|
|
dir = "/tmp/muyue"
|
|
}
|
|
imageDir = filepath.Join(dir, "images")
|
|
os.MkdirAll(imageDir, 0755)
|
|
}
|
|
|
|
var imageCounter uint64
|
|
|
|
func saveImage(dataURI, filename, mimeType string) (string, error) {
|
|
parts := strings.SplitN(dataURI, ",", 2)
|
|
if len(parts) != 2 {
|
|
return "", fmt.Errorf("invalid data URI")
|
|
}
|
|
encoded := parts[1]
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
return "", fmt.Errorf("base64 decode: %w", err)
|
|
}
|
|
if len(decoded) > 10*1024*1024 {
|
|
return "", fmt.Errorf("image too large (max 10MB)")
|
|
}
|
|
|
|
id := fmt.Sprintf("%d-%d", time.Now().UnixMilli(), atomic.AddUint64(&imageCounter, 1))
|
|
ext := ".png"
|
|
switch mimeType {
|
|
case "image/jpeg":
|
|
ext = ".jpg"
|
|
case "image/webp":
|
|
ext = ".webp"
|
|
}
|
|
|
|
filePath := filepath.Join(imageDir, id+ext)
|
|
if err := os.WriteFile(filePath, decoded, 0600); err != nil {
|
|
return "", fmt.Errorf("write image: %w", err)
|
|
}
|
|
|
|
return id + ext, nil
|
|
}
|
|
|
|
func imagePath(id string) string {
|
|
return filepath.Join(imageDir, filepath.Base(id))
|
|
}
|
|
|
|
func cleanupImages(ids []string) {
|
|
for _, id := range ids {
|
|
p := imagePath(id)
|
|
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
|
|
_ = err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleServeImage(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
writeError(w, "GET only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
id := strings.TrimPrefix(r.URL.Path, "/api/images/")
|
|
if id == "" {
|
|
writeError(w, "image id required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filePath := imagePath(id)
|
|
if _, err := os.Stat(filePath); err != nil {
|
|
writeError(w, "image not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
ext := strings.ToLower(filepath.Ext(id))
|
|
switch ext {
|
|
case ".jpg", ".jpeg":
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
case ".png":
|
|
w.Header().Set("Content-Type", "image/png")
|
|
case ".webp":
|
|
w.Header().Set("Content-Type", "image/webp")
|
|
default:
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
}
|
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
|
|
|
http.ServeFile(w, r, filePath)
|
|
}
|