Add files via upload

This commit is contained in:
公明
2026-06-04 13:36:46 +08:00
committed by GitHub
parent 679a8192ae
commit 444f85b9c4
6 changed files with 28 additions and 99 deletions
+1 -2
View File
@@ -15,8 +15,7 @@ type VisionConfig struct {
JPEGQuality int `yaml:"jpeg_quality,omitempty" json:"jpeg_quality,omitempty"`
MaxPayloadBytes int64 `yaml:"max_payload_bytes,omitempty" json:"max_payload_bytes,omitempty"`
SkipPreprocessBelowBytes int64 `yaml:"skip_preprocess_below_bytes,omitempty" json:"skip_preprocess_below_bytes,omitempty"` // 0=始终压缩;默认 2MB 且长边已<=max_dimension 时原图直传
Detail string `yaml:"detail,omitempty" json:"detail,omitempty"` // low | high | auto
AllowedRoots []string `yaml:"allowed_roots,omitempty" json:"allowed_roots,omitempty"`
Detail string `yaml:"detail,omitempty" json:"detail,omitempty"` // low | high | auto
}
func (v VisionConfig) TimeoutSecondsEffective() int {
-3
View File
@@ -1548,9 +1548,6 @@ func updateVisionConfig(doc *yaml.Node, cfg config.VisionConfig) {
if strings.TrimSpace(cfg.Detail) != "" {
setStringInMap(visionNode, "detail", cfg.Detail)
}
if len(cfg.AllowedRoots) > 0 {
setStringSliceInMap(visionNode, "allowed_roots", cfg.AllowedRoots)
}
}
func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
+2 -3
View File
@@ -809,8 +809,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"jpeg_quality": map[string]interface{}{"type": "integer", "description": "JPEG 质量 60-100"},
"max_payload_bytes": map[string]interface{}{"type": "integer", "description": "送 API 体积上限(字节)"},
"skip_preprocess_below_bytes": map[string]interface{}{"type": "integer", "description": "低于该字节且尺寸合规时可原图直传;0=始终压缩"},
"detail": map[string]interface{}{"type": "string", "enum": []string{"low", "high", "auto"}, "description": "OpenAI 兼容 image detail"},
"allowed_roots": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "额外允许读取的绝对路径根"},
"detail": map[string]interface{}{"type": "string", "enum": []string{"low", "high", "auto"}, "description": "OpenAI 兼容 image detail"},
},
},
"AnalyzeImageToolCall": map[string]interface{}{
@@ -819,7 +818,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"properties": map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "图片路径(cwd、chat_uploads、result_storage_dir 或 allowed_roots 下)",
"description": "图片绝对路径或相对于进程工作目录的路径",
},
"question": map[string]interface{}{
"type": "string",
+9 -79
View File
@@ -7,35 +7,26 @@ import (
"strings"
)
const chatUploadsDirName = "chat_uploads"
var allowedImageExt = map[string]struct{}{
".png": {}, ".jpg": {}, ".jpeg": {}, ".webp": {}, ".gif": {},
".bmp": {}, ".tif": {}, ".tiff": {},
}
// PathOptions 图片路径白名单根目录
type PathOptions struct {
CWD string
ResultStorageDir string // 相对 CWD,如 tmp
ExtraRoots []string // vision.allowed_roots 绝对路径
}
// ResolveImagePath 解析并校验可读图片路径(防穿越、symlink 逃逸)。
func ResolveImagePath(path string, opt PathOptions) (string, error) {
// ResolveImagePath 解析并校验可读图片路径(支持任意目录;仍校验扩展名与常规文件)
func ResolveImagePath(path string, cwd string) (string, error) {
p := strings.TrimSpace(path)
if p == "" {
return "", fmt.Errorf("path is empty")
}
cwd := strings.TrimSpace(opt.CWD)
if cwd == "" {
cwdTrim := strings.TrimSpace(cwd)
if cwdTrim == "" {
var err error
cwd, err = os.Getwd()
cwdTrim, err = os.Getwd()
if err != nil {
return "", fmt.Errorf("getwd: %w", err)
}
}
cwdAbs, err := filepath.Abs(filepath.Clean(cwd))
cwdAbs, err := filepath.Abs(filepath.Clean(cwdTrim))
if err != nil {
return "", err
}
@@ -46,22 +37,16 @@ func ResolveImagePath(path string, opt PathOptions) (string, error) {
} else {
candidate = filepath.Clean(filepath.Join(cwdAbs, p))
}
candidate = normalizeAbsPath(candidate)
if candidate == "" {
resolved := normalizeAbsPath(candidate)
if resolved == "" {
return "", fmt.Errorf("invalid path")
}
ext := strings.ToLower(filepath.Ext(candidate))
ext := strings.ToLower(filepath.Ext(resolved))
if _, ok := allowedImageExt[ext]; !ok {
return "", fmt.Errorf("unsupported image extension %q", ext)
}
roots := buildAllowedRoots(cwdAbs, opt)
resolved, err := evalUnderAllowedRoots(candidate, roots)
if err != nil {
return "", err
}
st, err := os.Stat(resolved)
if err != nil {
return "", fmt.Errorf("stat: %w", err)
@@ -85,58 +70,3 @@ func normalizeAbsPath(p string) string {
}
return abs
}
func buildAllowedRoots(cwdAbs string, opt PathOptions) []string {
seen := make(map[string]struct{})
var roots []string
add := func(r string) {
r = strings.TrimSpace(r)
if r == "" {
return
}
abs := normalizeAbsPath(r)
if abs == "" {
return
}
if _, ok := seen[abs]; ok {
return
}
seen[abs] = struct{}{}
roots = append(roots, abs)
}
add(cwdAbs)
add(filepath.Join(cwdAbs, chatUploadsDirName))
rs := strings.TrimSpace(opt.ResultStorageDir)
if rs == "" {
rs = "tmp"
}
if filepath.IsAbs(rs) {
add(rs)
} else {
add(filepath.Join(cwdAbs, rs))
}
for _, r := range opt.ExtraRoots {
add(r)
}
return roots
}
func evalUnderAllowedRoots(candidate string, roots []string) (string, error) {
check := normalizeAbsPath(candidate)
for _, root := range roots {
if isUnderRoot(check, root) {
return candidate, nil
}
}
return "", fmt.Errorf("path %q is outside allowed directories", candidate)
}
func isUnderRoot(path, root string) bool {
path = filepath.Clean(path)
root = filepath.Clean(root)
if path == root {
return true
}
sep := string(filepath.Separator)
return strings.HasPrefix(path, root+sep)
}
+15 -6
View File
@@ -12,7 +12,7 @@ func TestResolveImagePath_underCWD(t *testing.T) {
if err := os.WriteFile(img, []byte{0x89, 0x50, 0x4e, 0x47}, 0o644); err != nil {
t.Fatal(err)
}
got, err := ResolveImagePath(img, PathOptions{CWD: dir, ResultStorageDir: "tmp"})
got, err := ResolveImagePath(img, dir)
if err != nil {
t.Fatal(err)
}
@@ -22,11 +22,20 @@ func TestResolveImagePath_underCWD(t *testing.T) {
}
}
func TestResolveImagePath_rejectsTraversal(t *testing.T) {
func TestResolveImagePath_absoluteOutsideCWD(t *testing.T) {
dir := t.TempDir()
_, err := ResolveImagePath("../../../etc/passwd", PathOptions{CWD: dir})
if err == nil {
t.Fatal("expected error for path outside roots")
cwd := t.TempDir()
img := filepath.Join(dir, "remote.png")
if err := os.WriteFile(img, []byte{0x89, 0x50, 0x4e, 0x47}, 0o644); err != nil {
t.Fatal(err)
}
got, err := ResolveImagePath(img, cwd)
if err != nil {
t.Fatalf("expected absolute path outside cwd to be allowed: %v", err)
}
want := normalizeAbsPath(img)
if got != want {
t.Fatalf("got %q want %q", got, want)
}
}
@@ -36,7 +45,7 @@ func TestResolveImagePath_rejectsNonImageExt(t *testing.T) {
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
_, err := ResolveImagePath(f, PathOptions{CWD: dir})
_, err := ResolveImagePath(f, dir)
if err == nil {
t.Fatal("expected error for non-image extension")
}
+1 -6
View File
@@ -33,11 +33,6 @@ func RegisterAnalyzeImageTool(mcpServer *mcp.Server, cfg *config.Config, logger
return
}
pathOpt := PathOptions{
CWD: cwd,
ResultStorageDir: cfg.Agent.ResultStorageDir,
ExtraRoots: cfg.Vision.AllowedRoots,
}
preOpt := PreprocessOptions{
MaxImageBytes: cfg.Vision.MaxImageBytesEffective(),
MaxDimension: cfg.Vision.MaxDimensionEffective(),
@@ -73,7 +68,7 @@ func RegisterAnalyzeImageTool(mcpServer *mcp.Server, cfg *config.Config, logger
path, _ := args["path"].(string)
question, _ := args["question"].(string)
abs, err := ResolveImagePath(path, pathOpt)
abs, err := ResolveImagePath(path, cwd)
if err != nil {
return textResult(fmt.Sprintf("路径校验失败: %v", err), true), nil
}