feat(url): 支持docreader不上传替换白名单url的图片

This commit is contained in:
Li Xianggang
2026-05-18 17:25:22 +08:00
committed by lyingbug
parent b3dbf86bf5
commit c19d3543c8
4 changed files with 70 additions and 12 deletions
+5
View File
@@ -493,6 +493,11 @@ DOCREADER_TRANSPORT=grpc
# Weaviate gRPC 地址(Docker 内:weaviate:50051;宿主机访问:localhost:50052) # Weaviate gRPC 地址(Docker 内:weaviate:50051;宿主机访问:localhost:50052)
# WEAVIATE_GRPC_ADDRESS=weaviate:50051 # WEAVIATE_GRPC_ADDRESS=weaviate:50051
# 保留原始 URL 的图片域名白名单(可选,逗号分隔)
# 配置后,这些域名的图片仍会被下载和分析(OCR/字幕),但 markdown 中保留原始 URL,
# 不会被替换为对象存储的 provider:// URL。适用于内部稳定服务(如自建 MinerU)。
# IMAGE_HOST_KEEP_URL=mineru.internal.example.com
# Weaviate 架构模式 # Weaviate 架构模式
# WEAVIATE_SCHEME=http # WEAVIATE_SCHEME=http
+2
View File
@@ -43,6 +43,8 @@ NEO4J_ENABLE=false
WEKNORA_SANDBOX_MODE=disabled WEKNORA_SANDBOX_MODE=disabled
ENABLE_GRAPH_RAG=false ENABLE_GRAPH_RAG=false
DISABLE_REGISTRATION=false DISABLE_REGISTRATION=false
# 保留原始 URL 的图片域名白名单(可选)
# IMAGE_HOST_KEEP_URL=mineru.internal.example.com
# === Langfuse 可观测性(可选) === # === Langfuse 可观测性(可选) ===
# 追踪 chat / embedding / rerank / VLM / ASR 的 prompt、响应与 token 消耗。 # 追踪 chat / embedding / rerank / VLM / ASR 的 prompt、响应与 token 消耗。
+2
View File
@@ -149,6 +149,8 @@ services:
- TENANT_AES_KEY=${TENANT_AES_KEY:-} - TENANT_AES_KEY=${TENANT_AES_KEY:-}
- SYSTEM_AES_KEY=${SYSTEM_AES_KEY:-} - SYSTEM_AES_KEY=${SYSTEM_AES_KEY:-}
- SSRF_WHITELIST=${SSRF_WHITELIST:-} - SSRF_WHITELIST=${SSRF_WHITELIST:-}
# 保留原始 URL 的图片域名白名单(逗号分隔,不替换为 provider://)
- IMAGE_HOST_KEEP_URL=${IMAGE_HOST_KEEP_URL:-}
# Always allow the optional searxng sidecar (compose service hostname); # Always allow the optional searxng sidecar (compose service hostname);
# merged on top of SSRF_WHITELIST so user overrides don't clobber it. # merged on top of SSRF_WHITELIST so user overrides don't clobber it.
- SSRF_WHITELIST_EXTRA=${SSRF_WHITELIST_EXTRA:-searxng} - SSRF_WHITELIST_EXTRA=${SSRF_WHITELIST_EXTRA:-searxng}
@@ -13,6 +13,8 @@ import (
"log" "log"
"mime" "mime"
"net/http" "net/http"
"net/url"
"os"
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -195,6 +197,35 @@ func isProviderScheme(p string) bool {
return false return false
} }
// isWhitelistedImageHost checks if the image URL's host is in the whitelist.
// Whitelisted hosts are trusted (e.g. internal MinerU service) — images are
// still downloaded for validation and OCR/caption analysis, but not uploaded
// to object storage. The markdown keeps the original URL.
// Configure via IMAGE_HOST_KEEP_URL env var (comma-separated hosts).
func isWhitelistedImageHost(rawURL string) bool {
whitelist := strings.TrimSpace(os.Getenv("IMAGE_HOST_KEEP_URL"))
if whitelist == "" {
return false
}
u, err := url.Parse(rawURL)
if err != nil || u.Host == "" {
return false
}
host := strings.ToLower(u.Host)
hostname := strings.ToLower(u.Hostname())
for _, h := range strings.Split(whitelist, ",") {
h = strings.ToLower(strings.TrimSpace(h))
if h == "" {
continue
}
// Exact host match (includes port) or hostname match (any port)
if host == h || hostname == h {
return true
}
}
return false
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helper functions for base64 image handling // Helper functions for base64 image handling
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -670,10 +701,18 @@ func (r *ImageResolver) ResolveRemoteImages(
continue continue
} }
// --- SSRF check (centralised entry-point with whitelist support) --- // For whitelisted hosts: download to validate (mime type, icon check),
if err := secutils.ValidateURLForSSRF(imgURL); err != nil { // create StoredImage for downstream OCR/caption analysis, but do NOT
log.Printf("WARN: remote image blocked by SSRF check (%v): %s", err, imgURL) // upload to storage and keep the original URL in markdown.
continue // The multimodal service will download from the original URL later.
whitelisted := isWhitelistedImageHost(imgURL)
// --- SSRF check (skip for whitelisted) ---
if !whitelisted {
if err := secutils.ValidateURLForSSRF(imgURL); err != nil {
log.Printf("WARN: remote image blocked by SSRF check (%v): %s", err, imgURL)
continue
}
} }
// --- Download --- // --- Download ---
@@ -697,12 +736,20 @@ func (r *ImageResolver) ResolveRemoteImages(
ext = ".png" // safe default ext = ".png" // safe default
} }
// --- Upload to storage --- var servingURL string
fileName := uuid.New().String() + ext if whitelisted {
servingURL, saveErr := fileSvc.SaveBytes(ctx, data, tenantID, fileName, false) // Keep the original URL — ImageMultimodalService will download it
if saveErr != nil { // directly for OCR/caption analysis.
log.Printf("WARN: failed to save remote image %s: %v", imgURL, saveErr) servingURL = imgURL
continue } else {
// --- Upload to storage ---
fileName := uuid.New().String() + ext
var saveErr error
servingURL, saveErr = fileSvc.SaveBytes(ctx, data, tenantID, fileName, false)
if saveErr != nil {
log.Printf("WARN: failed to save remote image %s: %v", imgURL, saveErr)
continue
}
} }
images = append(images, StoredImage{ images = append(images, StoredImage{
@@ -711,8 +758,10 @@ func (r *ImageResolver) ResolveRemoteImages(
MimeType: mimeType, MimeType: mimeType,
}) })
// Replace URL in markdown. if !whitelisted {
markdown = markdown[:m[4]] + servingURL + markdown[m[5]:] // Replace URL in markdown.
markdown = markdown[:m[4]] + servingURL + markdown[m[5]:]
}
processed++ processed++
} }