Skip to content

Commit 259d9ce

Browse files
committed
fix: improve localized generation error diagnostics
1 parent 609d734 commit 259d9ce

17 files changed

Lines changed: 515 additions & 79 deletions

File tree

backend/internal/diagnostic/errors.go

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import (
88
)
99

1010
var (
11-
requestIDPattern = regexp.MustCompile(`(?i)(?:request[_\s-]?id|x-api-request-id|x-oneapi-request-id)\s*[:=]\s*([A-Za-z0-9._:-]+)`)
12-
httpStatusPattern = regexp.MustCompile(`(?i)(?:gemini|openai)\s+http\s+(\d{3})`)
11+
requestIDPattern = regexp.MustCompile(`(?i)(?:request[_\s-]?id|x-api-request-id|x-oneapi-request-id)\s*[:=]\s*([A-Za-z0-9._:-]+)`)
12+
httpStatusPatterns = []*regexp.Regexp{
13+
regexp.MustCompile(`(?i)\b(?:[a-z0-9._/-]+\s+)?http\s+(\d{3})\b`),
14+
regexp.MustCompile(`(?i)\bstatus(?:\s+code)?\s*[:=]?\s*(\d{3})\b`),
15+
}
1316
)
1417

1518
type ErrorSummary struct {
@@ -42,11 +45,7 @@ func SummarizeErrorMessage(message string) ErrorSummary {
4245
UserMessage: "生成失败,请稍后重试。",
4346
}
4447

45-
if match := httpStatusPattern.FindStringSubmatch(rawMessage); len(match) == 2 {
46-
if status, err := strconv.Atoi(match[1]); err == nil {
47-
summary.HTTPStatus = status
48-
}
49-
}
48+
summary.HTTPStatus = extractHTTPStatus(rawMessage)
5049

5150
set := func(code, category, userMessage string, retryable bool) {
5251
summary.Type = code
@@ -71,35 +70,37 @@ func SummarizeErrorMessage(message string) ErrorSummary {
7170
set("prompt_optimize_model_missing", "local_config", "默认提示词优化缺少可用的对话模型,请先在设置中补全并保存。", false)
7271
case strings.Contains(raw, "未找到指定的 provider"):
7372
set("prompt_optimize_provider_missing", "local_config", "默认提示词优化对应的对话模型配置不存在,请先在设置中补全并保存。", false)
73+
case summary.HTTPStatus == 0 && (strings.Contains(raw, "permission denied") || strings.Contains(raw, "operation not permitted")):
74+
set("local_permission_denied", "local_storage", "本地文件或目录没有写入权限,请检查保存路径、磁盘权限或系统授权。", false)
7475
case strings.Contains(raw, "未在响应中找到图片数据"):
7576
summary.Detail = extractTextResponse(rawMessage)
7677
if summary.Detail != "" || strings.Contains(raw, "安全警告") || strings.Contains(raw, "无法") || strings.Contains(raw, "抱歉") {
7778
set("policy_blocked", "upstream_policy", "上游返回了文本但没有图片,可能触发了安全或合规限制。请调整提示词或参考图后重试。", false)
7879
} else {
7980
set("no_image_data", "upstream_response", "上游已返回响应,但里面没有图片数据。请稍后重试。", true)
8081
}
81-
case summary.HTTPStatus == 400:
82-
set("bad_request", "upstream_request", "请求参数不被上游接受,请检查提示词、模型或图片参数。", false)
83-
case summary.HTTPStatus == 401 || strings.Contains(raw, "unauthorized") || strings.Contains(raw, "invalid api key"):
84-
set("auth_failed", "upstream_auth", "上游鉴权失败,请检查 API Key 是否正确。", false)
85-
case summary.HTTPStatus == 403 || strings.Contains(raw, "quota"):
86-
if strings.Contains(raw, "quota") {
87-
set("quota_exceeded", "upstream_quota", "上游额度不足或已被限制,请检查账户余额、套餐或权限。", false)
88-
} else {
89-
set("forbidden", "upstream_auth", "上游拒绝了本次请求,请检查权限、模型可用性或账户状态。", false)
90-
}
82+
case strings.Contains(raw, "insufficient_quota"), strings.Contains(raw, "quota exceeded"), strings.Contains(raw, "额度不足"), strings.Contains(raw, "余额不足"):
83+
set("quota_exceeded", "upstream_quota", "上游额度不足或已被限制,请检查账户余额、套餐或权限。", false)
84+
case summary.HTTPStatus == 400 || strings.Contains(raw, "bad request"):
85+
set("bad_request", "upstream_request", "请求格式或参数不被上游接受,请检查提示词、模型、图片参数或请求内容。", false)
86+
case summary.HTTPStatus == 401 || strings.Contains(raw, "unauthorized") || strings.Contains(raw, "invalid api key") || strings.Contains(raw, "api key") && strings.Contains(raw, "invalid") || strings.Contains(raw, "token expired") || strings.Contains(raw, "令牌过期"):
87+
set("unauthorized", "upstream_auth", "上游鉴权失败,请检查 API Key、令牌是否正确,或是否已过期。", false)
88+
case summary.HTTPStatus == 403 || strings.Contains(raw, "forbidden") || strings.Contains(raw, "权限不足") || strings.Contains(raw, "insufficient permissions"):
89+
set("forbidden", "upstream_permission", "上游拒绝了本次请求,一般是权限不足、账户受限或模型无权访问。", false)
9190
case summary.HTTPStatus == 404 || strings.Contains(raw, "not found"):
92-
set("model_not_found", "upstream_request", "请求的模型或接口不存在,请检查模型配置。", false)
91+
set("not_found", "upstream_request", "请求的资源未找到,可能是模型、接口地址或上游端点不存在。", false)
9392
case summary.HTTPStatus == 408:
94-
set("upstream_timeout", "upstream_network", "上游等待超时,图片结果未能及时返回。请稍后重试。", true)
95-
case summary.HTTPStatus == 429 || strings.Contains(raw, "too many requests") || strings.Contains(raw, "负载已饱和") || strings.Contains(raw, "overloaded"):
93+
set("request_timeout", "upstream_network", "上游等待超时,图片结果未能及时返回。请稍后重试。", true)
94+
case summary.HTTPStatus == 413 || strings.Contains(raw, "request entity too large") || strings.Contains(raw, "payload too large") || strings.Contains(raw, "content too large") || strings.Contains(raw, "请求体太大"):
95+
set("request_too_large", "upstream_request", "请求体过大,请减少图片体积、参考图数量,或缩短过长的输入内容后重试。", false)
96+
case summary.HTTPStatus == 429 || strings.Contains(raw, "too many requests") || strings.Contains(raw, "rate limit") || strings.Contains(raw, "负载已饱和") || strings.Contains(raw, "overloaded"):
9697
set("rate_limited", "upstream_capacity", "上游当前较忙或触发限流,请稍后再试。", true)
97-
case summary.HTTPStatus == 500:
98-
set("upstream_internal_error", "upstream_server", "上游服务内部异常,请稍后重试。", true)
98+
case summary.HTTPStatus == 500 || strings.Contains(raw, "internal server error"):
99+
set("internal_server_error", "upstream_server", "上游服务内部异常,这通常是服务端问题,请稍后重试。", true)
99100
case summary.HTTPStatus == 502 || strings.Contains(raw, "bad gateway"):
100101
set("bad_gateway", "upstream_gateway", "上游网关异常,结果没有正常回传。请稍后重试。", true)
101-
case summary.HTTPStatus == 503:
102-
set("service_unavailable", "upstream_gateway", "上游服务暂时不可用,请稍后重试。", true)
102+
case summary.HTTPStatus == 503 || strings.Contains(raw, "service unavailable") || strings.Contains(raw, "temporarily unavailable") || strings.Contains(raw, "维护中"):
103+
set("service_unavailable", "upstream_server", "上游服务暂时不可用,可能在维护或过载中,请稍后重试。", true)
103104
case summary.HTTPStatus == 504:
104105
set("gateway_timeout", "upstream_gateway", "上游网关等待超时,结果没有及时回传。请稍后重试。", true)
105106
case strings.Contains(raw, "tls"):
@@ -117,6 +118,20 @@ func SummarizeErrorMessage(message string) ErrorSummary {
117118
return summary
118119
}
119120

121+
func extractHTTPStatus(message string) int {
122+
for _, pattern := range httpStatusPatterns {
123+
match := pattern.FindStringSubmatch(message)
124+
if len(match) != 2 {
125+
continue
126+
}
127+
status, err := strconv.Atoi(match[1])
128+
if err == nil {
129+
return status
130+
}
131+
}
132+
return 0
133+
}
134+
120135
func UserFacingMessage(summary ErrorSummary) string {
121136
message := strings.TrimSpace(summary.UserMessage)
122137
if message == "" {

backend/internal/diagnostic/errors_test.go

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,33 @@ func TestSummarizeErrorMessage_ExtractsHTTPStatusFromProviderErrors(t *testing.T
2424
message: "OpenAI HTTP 503 request_id=req-openai body=service temporarily unavailable",
2525
wantStatus: 503,
2626
wantCode: "service_unavailable",
27-
wantCategory: "upstream_gateway",
27+
wantCategory: "upstream_server",
2828
wantRetryable: true,
2929
},
30+
{
31+
name: "yunwu bad request",
32+
message: "Yunwu HTTP 400 request_id=req-yunwu body=invalid request payload",
33+
wantStatus: 400,
34+
wantCode: "bad_request",
35+
wantCategory: "upstream_request",
36+
wantRetryable: false,
37+
},
38+
{
39+
name: "generic status code too large",
40+
message: "request failed with status code 413",
41+
wantStatus: 413,
42+
wantCode: "request_too_large",
43+
wantCategory: "upstream_request",
44+
wantRetryable: false,
45+
},
46+
{
47+
name: "unauthorized status code",
48+
message: "status=401 invalid api key",
49+
wantStatus: 401,
50+
wantCode: "unauthorized",
51+
wantCategory: "upstream_auth",
52+
wantRetryable: false,
53+
},
3054
}
3155

3256
for _, tc := range tests {
@@ -48,6 +72,97 @@ func TestSummarizeErrorMessage_ExtractsHTTPStatusFromProviderErrors(t *testing.T
4872
}
4973
}
5074

75+
func TestSummarizeErrorMessage_HTTPStatusMappings(t *testing.T) {
76+
tests := []struct {
77+
name string
78+
message string
79+
wantCode string
80+
wantCategory string
81+
wantRetry bool
82+
}{
83+
{
84+
name: "403 forbidden",
85+
message: "Gemini HTTP 403 request_id=req-forbidden body=permission denied",
86+
wantCode: "forbidden",
87+
wantCategory: "upstream_permission",
88+
wantRetry: false,
89+
},
90+
{
91+
name: "404 not found",
92+
message: "OpenAI HTTP 404 request_id=req-not-found body=model not found",
93+
wantCode: "not_found",
94+
wantCategory: "upstream_request",
95+
wantRetry: false,
96+
},
97+
{
98+
name: "429 too many requests",
99+
message: "HTTP 429 body=too many requests",
100+
wantCode: "rate_limited",
101+
wantCategory: "upstream_capacity",
102+
wantRetry: true,
103+
},
104+
{
105+
name: "500 internal server error",
106+
message: "Yunwu HTTP 500 request_id=req-500 body=internal server error",
107+
wantCode: "internal_server_error",
108+
wantCategory: "upstream_server",
109+
wantRetry: true,
110+
},
111+
}
112+
113+
for _, tc := range tests {
114+
t.Run(tc.name, func(t *testing.T) {
115+
summary := SummarizeErrorMessage(tc.message)
116+
if summary.Code != tc.wantCode {
117+
t.Fatalf("Code = %q, want %q", summary.Code, tc.wantCode)
118+
}
119+
if summary.Category != tc.wantCategory {
120+
t.Fatalf("Category = %q, want %q", summary.Category, tc.wantCategory)
121+
}
122+
if summary.Retryable != tc.wantRetry {
123+
t.Fatalf("Retryable = %t, want %t", summary.Retryable, tc.wantRetry)
124+
}
125+
})
126+
}
127+
}
128+
129+
func TestSummarizeErrorMessage_ClassifiesLocalPermissionErrors(t *testing.T) {
130+
tests := []struct {
131+
name string
132+
message string
133+
wantCode string
134+
wantCategory string
135+
}{
136+
{
137+
name: "local storage permission denied",
138+
message: "保存原图失败: 创建本地文件失败: open /tmp/a.png: permission denied",
139+
wantCode: "local_permission_denied",
140+
wantCategory: "local_storage",
141+
},
142+
{
143+
name: "operation not permitted",
144+
message: "创建目录失败: mkdir /Users/demo/output: operation not permitted",
145+
wantCode: "local_permission_denied",
146+
wantCategory: "local_storage",
147+
},
148+
}
149+
150+
for _, tc := range tests {
151+
t.Run(tc.name, func(t *testing.T) {
152+
summary := SummarizeErrorMessage(tc.message)
153+
if summary.Code != tc.wantCode {
154+
t.Fatalf("Code = %q, want %q", summary.Code, tc.wantCode)
155+
}
156+
if summary.Category != tc.wantCategory {
157+
t.Fatalf("Category = %q, want %q", summary.Category, tc.wantCategory)
158+
}
159+
if summary.Retryable {
160+
t.Fatalf("Retryable = %t, want false", summary.Retryable)
161+
}
162+
})
163+
}
164+
}
165+
51166
func TestSummarizeErrorMessage_ClassifiesPromptOptimizeConfigErrors(t *testing.T) {
52167
tests := []struct {
53168
name string

desktop/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "nano-banana-pro-frontend",
33
"private": true,
4-
"version": "2.6.2",
4+
"version": "2.6.3",
55
"license": "MIT",
66
"description": "大香蕉图片生成工具 - 批量图片生成应用前端",
77
"type": "module",

desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "desktop"
3-
version = "2.6.2"
3+
version = "2.6.3"
44
description = "A Tauri App"
55
authors = ["you"]
66
edition = "2021"

desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "大香蕉 AI",
4-
"version": "2.6.2",
4+
"version": "2.6.3",
55
"identifier": "com.dztool.banana",
66
"build": {
77
"beforeDevCommand": "npm run dev",

0 commit comments

Comments
 (0)