Compare commits

..
Author SHA1 Message Date
easyai 152f9d1206 chore(deploy): 前移生产迁移基线至 v0.4.2
ci / verify (pull_request) Successful in 9m42s
2026-07-22 05:32:39 +08:00
easyai d95cecd0eb Merge pull request #19: 修复 Gemini 兼容平台版本路径重复拼接
ci / verify (push) Successful in 9m45s
release-ci / verify-tag (push) Successful in 9m54s
修复 Gemini 兼容平台已包含 /v1 时重复追加 /v1beta 的问题。

验证:精确 ci / verify (pull_request) 已通过。
2026-07-22 05:04:12 +08:00
easyai 5954afef55 fix(gemini): 保留兼容平台的 API 版本路径
ci / verify (pull_request) Successful in 9m42s
2026-07-22 04:53:42 +08:00
easyai 929a7a172c Merge pull request #18: 修复 Gemini 图像默认参数与 server-main 脚本兼容
ci / verify (push) Successful in 9m44s
release-ci / verify-tag (push) Successful in 9m46s
合并已通过 ci / verify (pull_request) 的图像参数与脚本兼容修复。
2026-07-22 04:17:44 +08:00
easyai 16149260c7 fix(universal): 映射脚本上游模型名
ci / verify (pull_request) Successful in 9m51s
让 server-main 参数预处理兼容上下文使用平台绑定的真实模型名,避免把用户侧别名提交给上游。
2026-07-22 03:56:00 +08:00
easyai c72c43aaaa fix(universal): 兼容 server-main 脚本语义
ci / verify (pull_request) Successful in 9m52s
补充历史脚本依赖的参数预处理上下文,并识别嵌套同步结果,使迁移后的 Universal 图片通道可按原配置执行。
2026-07-22 03:50:59 +08:00
easyai 84aea01b5b build(web): 修复高危间接依赖漏洞
ci / verify (pull_request) Successful in 9m48s
将 fast-uri 约束到已修复的 3.1.3,确保高危依赖审计通过。
2026-07-22 03:39:48 +08:00
easyai 293ef24bb7 build(api): 升级存在漏洞的 Go 依赖 2026-07-22 03:37:34 +08:00
easyai 0f1765b226 fix(gemini): 初始化图像生成配置对象 2026-07-22 03:37:28 +08:00
easyai 1fa58ba901 Merge pull request #17: 前移生产迁移基线至 v0.4.0
ci / verify (push) Successful in 10m15s
生产版本 v0.4.0 已完成发布、健康检查和恢复演练,前移生产迁移安全基线。
2026-07-22 02:59:54 +08:00
easyai 55595570c2 chore(deploy): 前移生产迁移基线至 v0.4.0
ci / verify (pull_request) Successful in 10m18s
2026-07-22 02:48:38 +08:00
9 changed files with 148 additions and 21 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.uber.org/goleak v1.3.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/text v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+4 -4
View File
@@ -73,10 +73,10 @@ golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+52 -4
View File
@@ -1306,6 +1306,25 @@ func TestGeminiClientImageGenerateBuildsNativeImageBody(t *testing.T) {
}
}
func TestGeminiGenerationConfigInitializesMissingImageConfig(t *testing.T) {
config := geminiGenerationConfig(map[string]any{
"aspect_ratio": "16:9",
"resolution": "4K",
}, true)
imageConfig, ok := config["imageConfig"].(map[string]any)
if !ok {
t.Fatalf("imageConfig should be initialized, got %+v", config)
}
if imageConfig["aspectRatio"] != "16:9" || imageConfig["imageSize"] != "4K" {
t.Fatalf("unexpected imageConfig: %+v", imageConfig)
}
modalities, ok := config["responseModalities"].([]any)
if !ok || len(modalities) != 1 || modalities[0] != "IMAGE" {
t.Fatalf("image response modality should be initialized, got %+v", config)
}
}
func TestGeminiClientImageEditPreservesNativeContentsAndFileData(t *testing.T) {
var captured map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -1539,10 +1558,39 @@ func TestGeminiClientChatConvertsFunctionCallResponse(t *testing.T) {
}
func TestGeminiURLAcceptsVersionedBaseURL(t *testing.T) {
got := geminiURL("https://generativelanguage.googleapis.com/v1beta", "gemini-2.5-flash", "test-key")
want := "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key"
if got != want {
t.Fatalf("unexpected gemini url: %s", got)
tests := []struct {
name string
baseURL string
want string
}{
{
name: "default version",
baseURL: "https://generativelanguage.googleapis.com",
want: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key",
},
{
name: "google beta version",
baseURL: "https://generativelanguage.googleapis.com/v1beta",
want: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key",
},
{
name: "compatible v1 endpoint",
baseURL: "https://cloud.dataeyes.ai/v1",
want: "https://cloud.dataeyes.ai/v1/models/gemini-2.5-flash:generateContent?key=test-key",
},
{
name: "openai suffix after version",
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
want: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := geminiURL(test.baseURL, "gemini-2.5-flash", "test-key")
if got != test.want {
t.Fatalf("unexpected gemini url: %s", got)
}
})
}
}
+14 -3
View File
@@ -61,11 +61,13 @@ func geminiURL(baseURL string, model string, apiKey string) string {
base = "https://generativelanguage.googleapis.com"
}
base = strings.TrimSuffix(base, "/openai")
if strings.HasSuffix(base, "/v1beta") {
base = strings.TrimSuffix(base, "/v1beta")
if !strings.HasSuffix(base, "/v1") &&
!strings.HasSuffix(base, "/v1beta") &&
!strings.HasSuffix(base, "/v1alpha") {
base += "/v1beta"
}
escapedModel := url.PathEscape(model)
return fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", base, escapedModel, url.QueryEscape(apiKey))
return fmt.Sprintf("%s/models/%s:generateContent?key=%s", base, escapedModel, url.QueryEscape(apiKey))
}
func geminiBody(request Request) map[string]any {
@@ -195,14 +197,23 @@ func geminiApplyRequestOptions(body map[string]any, request Request, imageRespon
func geminiGenerationConfig(body map[string]any, imageResponse bool) map[string]any {
source := mapFromAny(firstPresent(body["generationConfig"], body["generation_config"]))
out := cloneMapAny(source)
if out == nil {
out = map[string]any{}
}
if aspectRatio := firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"]); aspectRatio != "" {
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
if imageConfig == nil {
imageConfig = map[string]any{}
}
imageConfig["aspectRatio"] = aspectRatio
out["imageConfig"] = imageConfig
delete(out, "image_config")
}
if imageSize := firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"]); imageSize != "" {
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
if imageConfig == nil {
imageConfig = map[string]any{}
}
imageConfig["imageSize"] = imageSize
out["imageConfig"] = imageConfig
delete(out, "image_config")
+25
View File
@@ -44,6 +44,8 @@ func (c UniversalClient) Run(ctx context.Context, request Request) (Response, er
if err != nil {
return Response{}, annotateResponseError(err, submitRequestID, startedAt, time.Now())
}
submitResult = universalEffectiveResult(submitResult)
submitRequestID = firstNonEmptyString(submitRequestID, requestIDFromResult(submitResult))
if isUniversalSuccess(submitResult) && submitResult["data"] != nil {
return Response{
Result: normalizeUniversalResult(request, submitResult, ""),
@@ -157,6 +159,7 @@ func (c UniversalClient) universalPollUntilDone(ctx context.Context, executor *s
if err != nil {
return nil, "", annotateResponseError(err, firstNonEmptyString(pollRequestID, requestID, upstreamTaskID), pollStarted, pollFinished)
}
result = universalEffectiveResult(result)
lastResult = result
requestID = firstNonEmptyString(pollRequestID, requestID, requestIDFromResult(result), upstreamTaskID)
if isUniversalSuccess(result) {
@@ -239,6 +242,14 @@ func universalScriptContext(request Request, modelType string, payload map[strin
return selectedBase + "/" + strings.TrimLeft(path, "/")
}
context["creatRequestURL"] = context["createRequestURL"]
processedParams := cloneBody(request.Body)
processedParams["model"] = upstreamModelName(request.Candidate)
context["processedParams"] = processedParams
context["preProcessParams"] = func(params map[string]any, _ ...string) map[string]any {
processed := cloneMapAny(params)
processed["model"] = upstreamModelName(request.Candidate)
return processed
}
context["resolveGetTaskURL"] = func(taskID string) string {
return resolveUniversalTaskURL(request.Candidate.PlatformConfig, taskID)
}
@@ -389,6 +400,20 @@ func universalStatus(result map[string]any) string {
return strings.ToLower(strings.TrimSpace(firstNonEmptyString(result["status"], result["state"], result["task_status"])))
}
func universalEffectiveResult(result map[string]any) map[string]any {
nested, ok := result["result"].(map[string]any)
if !ok || nested == nil || isUniversalFailure(result) {
return result
}
out := cloneMapAny(nested)
for _, key := range []string{"status", "request_id", "requestId", "upstream_task_id", "task_id", "taskId", "id"} {
if out[key] == nil && result[key] != nil {
out[key] = result[key]
}
}
return out
}
func universalTaskID(result map[string]any) string {
return firstNonEmptyString(result["upstream_task_id"], result["task_id"], result["taskId"], result["id"])
}
@@ -92,6 +92,47 @@ func TestUniversalClientDefaultSubmitAndPoll(t *testing.T) {
}
}
func TestUniversalClientSupportsServerMainScriptContextAndNestedResult(t *testing.T) {
request := Request{
Kind: "images.generations",
ModelType: "image_generate",
Model: "custom-image",
Body: map[string]any{"model": "custom-image", "prompt": "hello"},
Candidate: testUniversalCandidate(map[string]any{
"customGetParamsScript": map[string]any{
"image_generate": `async function getParams(params, context) {
const processed = await context.preProcessParams(params, context.type);
return { prompt: processed.prompt + "-" + processed.model + "-" + context.processedParams.model };
}`,
},
"customSubmitScript": map[string]any{
"image_generate": `async function submitTask(payload) {
return {
status: "success",
result: {
status: "success",
data: [{ url: "https://cdn.example/" + payload.prompt + ".png" }]
}
};
}`,
},
}),
}
response, err := (UniversalClient{}).Run(context.Background(), request)
if err != nil {
t.Fatalf("run failed: %v", err)
}
data, ok := response.Result["data"].([]any)
if !ok || len(data) != 1 {
t.Fatalf("unexpected nested result: %#v", response.Result)
}
image, ok := data[0].(map[string]any)
if !ok || image["url"] != "https://cdn.example/hello-provider-model-provider-model.png" {
t.Fatalf("unexpected image result: %#v", response.Result)
}
}
func TestUniversalClientResumeSkipsSubmit(t *testing.T) {
request := Request{
Kind: "videos.generations",
+1 -1
View File
@@ -1 +1 @@
2d6c16fec0bec9c0288e5cb142b458af982fff8f
d95cecd0ebac48bda7ff0a2dcf453bb02b85ca7e
+8 -7
View File
@@ -6,17 +6,18 @@ settings:
overrides:
'@babel/core@<=7.29.0': 7.29.6
'@nx/js>picomatch': 4.0.5
'@nx/vite>picomatch': 4.0.5
'@nx/workspace>picomatch': 4.0.5
axios@<1.18.0: 1.18.0
brace-expansion@>=2.0.0 <2.1.2: 2.1.2
dompurify@<=3.4.10: 3.4.11
esbuild@>=0.27.3 <0.28.1: 0.28.1
fast-uri@>=3.0.0 <3.1.3: 3.1.3
form-data@>=4.0.0 <4.0.6: 4.0.6
js-yaml@<4.3.0: 4.3.0
mermaid@>=11.0.0-alpha.1 <=11.14.0: 11.15.0
minimatch@>=9.0.0 <9.0.7: 9.0.7
'@nx/js>picomatch': 4.0.5
'@nx/vite>picomatch': 4.0.5
'@nx/workspace>picomatch': 4.0.5
tmp@<0.2.7: 0.2.7
importers:
@@ -3034,8 +3035,8 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
fast-uri@3.1.3:
resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
@@ -7181,7 +7182,7 @@ snapshots:
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.2
fast-uri: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -7850,7 +7851,7 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-uri@3.1.2: {}
fast-uri@3.1.3: {}
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
+1
View File
@@ -12,6 +12,7 @@ overrides:
'brace-expansion@>=2.0.0 <2.1.2': 2.1.2
'dompurify@<=3.4.10': 3.4.11
'esbuild@>=0.27.3 <0.28.1': 0.28.1
'fast-uri@>=3.0.0 <3.1.3': 3.1.3
'form-data@>=4.0.0 <4.0.6': 4.0.6
'js-yaml@<4.3.0': 4.3.0
'mermaid@>=11.0.0-alpha.1 <=11.14.0': 11.15.0