mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-03 12:10:28 +00:00
6b8a0f9521
Final review pass before taking the AI agent branch live. Knowledge base: - Text not wrapped in a block tag was never collected, so prose around a table or list never reached the index. The assistant answered "no relevant information" for questions the snippet covered. - Blocks over the token limit were truncated and the remainder dropped. They are split into several chunks now. - Trimming an oversized block ran one rune at a time and re-tokenized the whole string each step. A large table took minutes. It uses a binary search now. - Overlap text was not escaped, so a sentence containing markup swallowed the rest of the chunk. - SVG and template text no longer reaches the index. AI agent: - Verification codes are capped per address and per conversation. The cap was per conversation only, so a customer correcting a mistyped email was told to check an inbox that never got a code. - Livechat verification sends synchronously. A queued send returned nil even when SMTP failed, so a failure counted as a sent code. - Queued jobs drain on shutdown and hand off to a human instead of being dropped with no reply. - Deleting an assistant no longer moves resolved and closed conversations into the fallback team. - Image decode is capped at 25 MP. The old bound allowed a 400 MB decode per attachment. Auth and admin: - A blank OIDC client secret no longer overwrites the stored one. Blank id or secret is rejected instead. - OIDC token exchange uses the SSRF guarded client with a timeout. - Renaming a tool auth header no longer attaches the secret of whichever row now sits at that position. - Clearing embedding dimensions no longer refills 1536 on the next load, which pushed a wrong value to the provider on the next save. - Copilot conversation lookups filter by access before capping at 10.
103 lines
3.1 KiB
Go
103 lines
3.1 KiB
Go
// Package image provides utilities for processing image files, including
|
|
// retrieving image dimensions and creating thumbnails.
|
|
package image
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/gabriel-vasile/mimetype"
|
|
)
|
|
|
|
const (
|
|
// llmMaxDim caps an image's longest edge before it is sent to a vision model.
|
|
llmMaxDim = 1568
|
|
llmJPEGQuality = 85
|
|
// maxDecodePixels bounds width*height read from the header, blocking a small file that declares huge dimensions. 25 MP is ~100 MB of RGBA per agent worker.
|
|
maxDecodePixels = 25_000_000
|
|
)
|
|
|
|
var (
|
|
Exts = []string{"gif", "png", "jpg", "jpeg"}
|
|
DefThumbSize = 150
|
|
ThumbPrefix = "thumb_"
|
|
)
|
|
|
|
// IsImageByContent returns true when the file's magic bytes identify it as one
|
|
// of the raster formats this package can decode. Used as a fallback when the
|
|
// filename has no extension or an unreliable one (e.g. attachments arriving
|
|
// through email without proper file extensions).
|
|
func IsImageByContent(r io.ReadSeeker) bool {
|
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
|
return false
|
|
}
|
|
defer r.Seek(0, io.SeekStart)
|
|
mtype, err := mimetype.DetectReader(r)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
switch mtype.String() {
|
|
case "image/png", "image/jpeg", "image/gif":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetDimensions returns the width and height of the image in the provided file.
|
|
// It returns an error if the image cannot be decoded.
|
|
func GetDimensions(r io.Reader) (int, int, error) {
|
|
img, err := imaging.Decode(r)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
width := bounds.Max.X
|
|
height := bounds.Max.Y
|
|
|
|
return width, height, nil
|
|
}
|
|
|
|
// CreateThumb generates a thumbnail of the given image file with the specified maximum dimension.
|
|
// The thumbnail's width will be resized to `thumbPxSize` while maintaining the aspect ratio.
|
|
func CreateThumb(thumbPxSize int, r io.Reader) (*bytes.Reader, error) {
|
|
img, err := imaging.Decode(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
thumb := imaging.Resize(img, thumbPxSize, 0, imaging.Lanczos)
|
|
var out bytes.Buffer
|
|
if err := imaging.Encode(&out, thumb, imaging.PNG); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bytes.NewReader(out.Bytes()), nil
|
|
}
|
|
|
|
// EncodeForLLM decodes an image, downscales its longest edge to at most llmMaxDim, re-encodes it as
|
|
// JPEG, and returns the base64 payload plus media type for a vision model request.
|
|
func EncodeForLLM(content []byte) (data string, mediaType string, err error) {
|
|
cfg, _, err := image.DecodeConfig(bytes.NewReader(content))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width > maxDecodePixels/cfg.Height {
|
|
return "", "", fmt.Errorf("invalid or too-large image dimensions %dx%d", cfg.Width, cfg.Height)
|
|
}
|
|
img, err := imaging.Decode(bytes.NewReader(content))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
img = imaging.Fit(img, llmMaxDim, llmMaxDim, imaging.Lanczos)
|
|
var out bytes.Buffer
|
|
if err := imaging.Encode(&out, img, imaging.JPEG, imaging.JPEGQuality(llmJPEGQuality)); err != nil {
|
|
return "", "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(out.Bytes()), "image/jpeg", nil
|
|
}
|