mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
Accumulated work from the branch, carried across the rebase onto origin/studio and merged with what landed there in the meantime. Storage: a workspace can name where its state lives, and the server tracks the one relocation it may be running (internal/storage, config/storage_roots, serve/storage, the Storage panel). Composer intake: what a drop or a long paste becomes is one pure function — payload in, action out — so "what happens if I drop a folder" is a test rather than a branch of an onDrop handler. It sits on the drop router that landed upstream: that one answers which pane a drop hit, this one answers what it means and says so before the pointer is released. A paste long enough to bury the composer becomes a chip beside it instead. Session: the checkpoint agreement and the lineage/torn-log recovery cases, plus the conflict diagnostic learning where two transcripts parted and which roles stood there — a position and a role tell a local reshape from a foreign write without recording any content. Model refs: the default-model write checks the live catalog, so an extension model the runtime knows can be persisted as the default.
90 lines
3.2 KiB
Go
90 lines
3.2 KiB
Go
package serve
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"reasonix/internal/control"
|
|
)
|
|
|
|
// Bytes ride into a turn as path references, exactly as the CLI's do. This is
|
|
// the door for a client that has bytes and no path: a browser tab, and the
|
|
// clipboard everywhere. A window that knows the path uses POST /drop and copies
|
|
// nothing. JSON rather than raw bytes because csrfGuard admits nothing else.
|
|
// control enforces the real per-kind limit once these are decoded.
|
|
const maxAttachmentUpload = 25 << 20
|
|
|
|
// attachmentBytes takes the bytes from whichever half the client had. A desktop
|
|
// drop reports a path and never the bytes, so the host reads the file it was
|
|
// pointed at — bounded to a regular file under the size cap, and still handed to
|
|
// the same saver, which admits nothing that does not sniff as a real image.
|
|
func attachmentBytes(path, data string) ([]byte, error) {
|
|
if strings.TrimSpace(path) == "" {
|
|
raw, err := base64.StdEncoding.DecodeString(data)
|
|
if err != nil {
|
|
return nil, errors.New("attachment data must be base64")
|
|
}
|
|
return raw, nil
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read attachment: %w", err)
|
|
}
|
|
// A directory or a device would read as something no image sniff would
|
|
// admit, but refusing here says what was wrong instead of what it was not.
|
|
if !info.Mode().IsRegular() {
|
|
return nil, errors.New("attachment path is not a regular file")
|
|
}
|
|
if info.Size() > maxAttachmentUpload {
|
|
return nil, errors.New("attachment is larger than 10 MB")
|
|
}
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
func (s *Server) attachments(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, (maxAttachmentUpload/3)*4+1024)
|
|
var body struct {
|
|
Mime string `json:"mime"`
|
|
// Name only supplies the extension the bytes are stored under; the
|
|
// stored name is generated either way.
|
|
Name string `json:"name"`
|
|
Data string `json:"data"`
|
|
Path string `json:"path"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "read attachment: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
raw, err := attachmentBytes(body.Path, body.Data)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
root := s.ctl().WorkspaceRoot()
|
|
// The declared type picks the door, not the name: bytes claiming to be a
|
|
// picture must prove it, because a turn referencing them asks a model to
|
|
// look at them. Everything else is stored as the file it says it is.
|
|
save := func() (string, error) { return control.SaveAttachmentBytesInRoot(root, body.Name, raw) }
|
|
if body.Name == "" || strings.HasPrefix(strings.ToLower(body.Mime), "image/") {
|
|
save = func() (string, error) { return control.SaveImageBytesInRoot(root, body.Mime, raw) }
|
|
}
|
|
saved, err := save()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// The turn parser resolves "@<path>"; hand back the exact token so the
|
|
// client never reconstructs the reference syntax itself.
|
|
rel := saved
|
|
if r, relErr := filepath.Rel(root, saved); relErr == nil && !strings.HasPrefix(r, "..") {
|
|
rel = filepath.ToSlash(r)
|
|
}
|
|
writeJSON(w, map[string]any{"path": rel, "ref": "@" + rel, "image": control.RefIsImage(rel)})
|
|
}
|