返回 DeepSeek-Reasonix
refs_images.go
根目录 / internal / control / refs_images.go
1 package control
2
3 import (
4 "encoding/base64"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9 )
10
11 // inputImages resolves image @-references in the turn input to data URLs so the
12 // turn can carry them to a vision-capable model. Best-effort: an unreadable image
13 // is skipped — the @ref still lands as text via ResolveRefs.
14 func (c *Controller) inputImages(line string) []string {
15 if !c.imageInputEnabled() {
16 return nil
17 }
18 return c.resolveInputImageCandidates(line)
19 }
20
21 // resolveInputImageCandidates resolves authorized image references without
22 // consulting the active model capability. The parent controller uses this only
23 // to hand candidates to a child; the child decides whether to embed them.
24 func (c *Controller) resolveInputImageCandidates(line string) []string {
25 var urls []string
26 seen := map[string]bool{}
27 for _, r := range append(c.detectRefs(line), bareVisionRefs(line)...) {
28 url, err := c.resolveReferenceImage(r)
29 if err != nil || url == "" || seen[url] {
30 continue
31 }
32 seen[url] = true
33 urls = append(urls, url)
34 }
35 return urls
36 }
37
38 func (c *Controller) resolveReferenceImage(r ref) (string, error) {
39 baseDir := c.workspaceRoot
40 if r.baseDir != "" {
41 baseDir = r.baseDir
42 }
43 return c.visionRefImageValue(r, baseDir)
44 }
45
46 func visionFileImageDataURL(path, baseDir string) (string, error) {
47 absPath, absBase, ok := resolveAbsRef(path, baseDir)
48 if !ok {
49 return "", os.ErrNotExist
50 }
51 if absBase == "" {
52 return "", fmt.Errorf("workspace root is required for file image references")
53 }
54
55 root, err := os.OpenRoot(absBase)
56 if err != nil {
57 return "", err
58 }
59 defer root.Close()
60
61 rel, err := filepath.Rel(absBase, absPath)
62 if err != nil {
63 return "", err
64 }
65 info, err := root.Lstat(rel)
66 if err != nil {
67 return "", err
68 }
69 if info.Mode()&os.ModeSymlink != 0 {
70 return "", fmt.Errorf("image path must not be a symlink")
71 }
72 if info.IsDir() || info.Size() <= 0 || info.Size() > maxImageAttachmentBytes {
73 return "", fmt.Errorf("image must be between 1 byte and 64 MB")
74 }
75 f, err := root.Open(rel)
76 if err != nil {
77 return "", err
78 }
79 defer f.Close()
80 opened, err := f.Stat()
81 if err != nil {
82 return "", err
83 }
84 if !os.SameFile(info, opened) {
85 return "", fmt.Errorf("image changed while opening")
86 }
87 return dataURLFromImageReader(f, path)
88 }
89
90 func dataURLFromImageReader(r io.Reader, path string) (string, error) {
91 raw, err := io.ReadAll(io.LimitReader(r, maxImageAttachmentBytes+1))
92 if err != nil {
93 return "", err
94 }
95 if len(raw) == 0 || len(raw) > maxImageAttachmentBytes {
96 return "", fmt.Errorf("image must be between 1 byte and 64 MB")
97 }
98 mime := detectedImageMime(raw)
99 if mime == "" {
100 return "", fmt.Errorf("%s is not a supported image", path)
101 }
102 raw, mime = compressForVision(raw, mime)
103 return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(raw), nil
104 }
105
105 lines GO