Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
# 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
# STREAMING_TIMEOUT=300

# 异步图片中转允许的同步上游基地址(逗号分隔;路径仅允许空或 /v1)
# ASYNC_YUNWU_ALLOWED_BASE_URLS=https://yunwu.ai
# ASYNC_GRSAI_ALLOWED_BASE_URLS=https://grsaiapi.com,https://grsai.dakka.com.cn

# TLS / HTTP 跳过验证设置
# TLS_INSECURE_SKIP_VERIFY=false

Expand Down
86 changes: 86 additions & 0 deletions common/async_yunwu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package common

import (
"net/url"
"os"
"strings"
)

type AsyncImageProvider string

const (
AsyncImageProviderYunwu AsyncImageProvider = "yunwu"
AsyncImageProviderGRSAI AsyncImageProvider = "grsai"
)

// IsAllowedYunwuBaseURL restricts the async wrapper to an explicitly allowed
// Yunwu origin and the only two base paths that can produce the whitelisted
// /v1/images/generations endpoint.
func IsAllowedYunwuBaseURL(raw string) bool {
parsed, ok := normalizeAsyncImageBaseURL(raw)
if !ok {
return false
}
allowed := strings.TrimSpace(os.Getenv("ASYNC_YUNWU_ALLOWED_BASE_URLS"))
if allowed == "" {
return parsed == "https://yunwu.ai"
}
for _, item := range strings.Split(allowed, ",") {
candidate, valid := normalizeAsyncImageBaseURL(item)
if valid && parsed == candidate {
return true
}
}
return false
}

// IsAllowedGRSAIBaseURL limits GRS AI workers to the two documented API
// origins, unless an explicit allowlist is configured for integration tests or
// private relay nodes. The dashboard origin is intentionally not accepted.
func IsAllowedGRSAIBaseURL(raw string) bool {
parsed, ok := normalizeAsyncImageBaseURL(raw)
if !ok {
return false
}
allowed := strings.TrimSpace(os.Getenv("ASYNC_GRSAI_ALLOWED_BASE_URLS"))
if allowed == "" {
return parsed == "https://grsaiapi.com" || parsed == "https://grsai.dakka.com.cn"
}
for _, item := range strings.Split(allowed, ",") {
candidate, valid := normalizeAsyncImageBaseURL(item)
if valid && parsed == candidate {
return true
}
}
return false
}

func AsyncImageProviderForBaseURL(raw string) (AsyncImageProvider, bool) {
if IsAllowedYunwuBaseURL(raw) {
return AsyncImageProviderYunwu, true
}
if IsAllowedGRSAIBaseURL(raw) {
return AsyncImageProviderGRSAI, true
}
return "", false
}

func IsAllowedAsyncImageBaseURL(raw string) bool {
_, ok := AsyncImageProviderForBaseURL(raw)
return ok
}

func normalizeAsyncImageBaseURL(raw string) (string, bool) {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.User != nil || parsed.Hostname() == "" || parsed.RawQuery != "" || parsed.Fragment != "" {
return "", false
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", false
}
path := strings.TrimRight(parsed.Path, "/")
if path != "" && path != "/v1" {
return "", false
}
return strings.ToLower(parsed.Scheme + "://" + parsed.Host), true
}
1 change: 1 addition & 0 deletions constant/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type TaskPlatform string
const (
TaskPlatformSuno TaskPlatform = "suno"
TaskPlatformMidjourney = "mj"
TaskPlatformAsyncImage = "async_image"
)

const (
Expand Down
Loading
Loading