Skip to content

Commit e9c80c7

Browse files
DouDOU-startclaude
andcommitted
fix(plugin): preserve image routing metadata
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6abf7bf commit e9c80c7

4 files changed

Lines changed: 154 additions & 8 deletions

File tree

backend/internal/plugin/forwarder.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package plugin
33
import (
44
"log/slog"
55
"net/http"
6+
"strings"
67
"time"
78

89
"github.com/gin-gonic/gin"
@@ -78,14 +79,9 @@ func (f *Forwarder) Forward(c *gin.Context) {
7879
}
7980
defer releaseClientQuota()
8081

81-
routes, err := routing.ListEligibleGroups(c.Request.Context(), f.db, state.keyInfo.UserID, state.requestedPlatform, state.keyInfo.UserGroupRates, routing.Requirements{
82+
routes := routesForAPIKey(state, routing.Requirements{
8283
NeedsImage: requestNeedsImage(state.requestPath, state.model),
8384
})
84-
if err != nil {
85-
slog.Error("查询候选分组失败", "platform", state.requestedPlatform, "model", state.model, "error", err)
86-
openAIError(c, http.StatusServiceUnavailable, "server_error", "routing_unavailable", "请求暂时无法完成,请稍后重试")
87-
return
88-
}
8985
if len(routes) == 0 {
9086
slog.Warn("没有可用候选分组", "platform", state.requestedPlatform, "model", state.model, "user_id", state.keyInfo.UserID)
9187
openAIError(c, http.StatusServiceUnavailable, "server_error", "no_available_route", "请求暂时无法完成,请稍后重试")
@@ -198,6 +194,69 @@ func (f *Forwarder) Forward(c *gin.Context) {
198194
openAIError(c, 503, "server_error", "all_routes_failed", "请求暂时无法完成,请稍后重试")
199195
}
200196

197+
func routesForAPIKey(state *forwardState, requirements routing.Requirements) []routing.Candidate {
198+
if state == nil || state.keyInfo == nil {
199+
return nil
200+
}
201+
if !apiKeyGroupMatchesRequirements(state.keyInfo, requirements) {
202+
return nil
203+
}
204+
return []routing.Candidate{keyInfoRoute(state.keyInfo)}
205+
}
206+
207+
func apiKeyGroupMatchesRequirements(keyInfo *auth.APIKeyInfo, requirements routing.Requirements) bool {
208+
if keyInfo == nil {
209+
return false
210+
}
211+
if requirements.NeedsImage && strings.EqualFold(keyInfo.GroupPlatform, "openai") {
212+
return pluginSettingEnabledForKey(keyInfo.GroupPluginSettings, "openai", "image_enabled")
213+
}
214+
return true
215+
}
216+
217+
func pluginSettingEnabledForKey(settings map[string]map[string]string, plugin, key string) bool {
218+
for pluginName, kv := range settings {
219+
if !strings.EqualFold(pluginName, plugin) {
220+
continue
221+
}
222+
for k, v := range kv {
223+
if strings.EqualFold(k, key) {
224+
return strings.EqualFold(strings.TrimSpace(v), "true")
225+
}
226+
}
227+
}
228+
return false
229+
}
230+
231+
func keyInfoRoute(keyInfo *auth.APIKeyInfo) routing.Candidate {
232+
return routing.Candidate{
233+
GroupID: keyInfo.GroupID,
234+
Platform: keyInfo.GroupPlatform,
235+
EffectiveRate: billing.ResolveBillingRateForGroup(keyInfo.UserGroupRates, keyInfo.GroupID, keyInfo.GroupRateMultiplier),
236+
GroupRateMultiplier: keyInfo.GroupRateMultiplier,
237+
GroupServiceTier: keyInfo.GroupServiceTier,
238+
GroupForceInstructions: keyInfo.GroupForceInstructions,
239+
GroupPluginSettings: clonePluginSettingsForKey(keyInfo.GroupPluginSettings),
240+
}
241+
}
242+
243+
func clonePluginSettingsForKey(in map[string]map[string]string) map[string]map[string]string {
244+
if len(in) == 0 {
245+
return nil
246+
}
247+
out := make(map[string]map[string]string, len(in))
248+
for plugin, settings := range in {
249+
if len(settings) == 0 {
250+
continue
251+
}
252+
out[plugin] = make(map[string]string, len(settings))
253+
for k, v := range settings {
254+
out[plugin][k] = v
255+
}
256+
}
257+
return out
258+
}
259+
201260
func keyInfoForRoute(base *auth.APIKeyInfo, route routing.Candidate) *auth.APIKeyInfo {
202261
info := *base
203262
info.GroupID = route.GroupID

backend/internal/plugin/forwarder_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package plugin
22

33
import (
44
"testing"
5+
6+
"github.com/DouDOU-start/airgate-core/internal/auth"
7+
"github.com/DouDOU-start/airgate-core/internal/routing"
58
)
69

710
func TestParseBody(t *testing.T) {
@@ -37,3 +40,53 @@ func TestParseBody_StreamTrue(t *testing.T) {
3740
t.Fatalf("SessionID = %q, want %q", parsed.SessionID, "sess-1")
3841
}
3942
}
43+
44+
func TestRoutesForAPIKeyUsesBoundGroupOnly(t *testing.T) {
45+
t.Parallel()
46+
47+
settings := map[string]map[string]string{"openai": {"image_enabled": "true"}}
48+
state := &forwardState{keyInfo: &auth.APIKeyInfo{
49+
GroupID: 42,
50+
GroupPlatform: "openai",
51+
GroupRateMultiplier: 1.5,
52+
UserGroupRates: map[int64]float64{42: 0.7, 99: 0.1},
53+
GroupPluginSettings: settings,
54+
GroupServiceTier: "priority",
55+
GroupForceInstructions: "stay concise",
56+
}}
57+
58+
routes := routesForAPIKey(state, routing.Requirements{NeedsImage: true})
59+
if len(routes) != 1 {
60+
t.Fatalf("len(routes) = %d, want 1", len(routes))
61+
}
62+
route := routes[0]
63+
if route.GroupID != 42 {
64+
t.Fatalf("GroupID = %d, want 42", route.GroupID)
65+
}
66+
if route.EffectiveRate != 0.7 {
67+
t.Fatalf("EffectiveRate = %v, want 0.7", route.EffectiveRate)
68+
}
69+
if route.GroupPluginSettings["openai"]["image_enabled"] != "true" {
70+
t.Fatalf("image_enabled not preserved")
71+
}
72+
73+
settings["openai"]["image_enabled"] = "false"
74+
if route.GroupPluginSettings["openai"]["image_enabled"] != "true" {
75+
t.Fatalf("route plugin settings should be cloned")
76+
}
77+
}
78+
79+
func TestRoutesForAPIKeyRejectsImageWhenBoundGroupDisabled(t *testing.T) {
80+
t.Parallel()
81+
82+
state := &forwardState{keyInfo: &auth.APIKeyInfo{
83+
GroupID: 42,
84+
GroupPlatform: "openai",
85+
GroupPluginSettings: map[string]map[string]string{"openai": {"image_enabled": "false"}},
86+
}}
87+
88+
routes := routesForAPIKey(state, routing.Requirements{NeedsImage: true})
89+
if len(routes) != 0 {
90+
t.Fatalf("len(routes) = %d, want 0", len(routes))
91+
}
92+
}

web/src/i18n/en.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,11 @@
985985
"no_response": "No response from AI service",
986986
"send": "Send",
987987
"stop": "Stop",
988+
"edit": "Edit",
989+
"image": "Image",
990+
"attach_images": "Attach images",
991+
"select_model_first": "Select a model first",
992+
"select_image_model_first": "Select an image model first",
988993
"input_placeholder": "Type a message...",
989994
"input_hint": "Enter to send, Shift+Enter for newline",
990995
"platform": "Platform",
@@ -1004,6 +1009,18 @@
10041009
"retry": "Retry",
10051010
"regenerate": "Regenerate",
10061011
"retry_image": "Retry image",
1007-
"no_image_prompt": "No prompt found for this image"
1012+
"no_image_prompt": "No prompt found for this image",
1013+
"edit_image_region": "Edit image region",
1014+
"edit_image_region_hint": "Drag on the image to box-select the area to replace.",
1015+
"choose_source_image_region_hint": "Choose a source image and select a region.",
1016+
"replace_source": "Replace source",
1017+
"choose_source": "Choose source",
1018+
"region_selected": "Region selected",
1019+
"drag_to_select": "Drag to select",
1020+
"clear_selection": "Clear selection",
1021+
"choose_source_image_for_regional_editing": "Choose a source image for regional editing",
1022+
"choose_source_image_first": "Choose a source image first",
1023+
"select_edit_area_first": "Select the area to edit first",
1024+
"describe_image_change_first": "Describe the image change first"
10081025
}
10091026
}

web/src/i18n/zh.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,11 @@
989989
"no_response": "AI 服务未返回结果",
990990
"send": "发送",
991991
"stop": "停止",
992+
"edit": "编辑",
993+
"image": "图片",
994+
"attach_images": "添加图片",
995+
"select_model_first": "请先选择模型",
996+
"select_image_model_first": "请先选择图片模型",
992997
"input_placeholder": "输入消息...",
993998
"input_hint": "Enter 发送,Shift+Enter 换行",
994999
"platform": "平台",
@@ -1008,6 +1013,18 @@
10081013
"retry": "重试",
10091014
"regenerate": "重新生成",
10101015
"retry_image": "重试图片生成",
1011-
"no_image_prompt": "未找到这张图片对应的提示词"
1016+
"no_image_prompt": "未找到这张图片对应的提示词",
1017+
"edit_image_region": "编辑图片区域",
1018+
"edit_image_region_hint": "在图片上拖拽框选要替换的区域。",
1019+
"choose_source_image_region_hint": "选择一张源图片并框选区域。",
1020+
"replace_source": "替换源图",
1021+
"choose_source": "选择源图",
1022+
"region_selected": "已选择区域",
1023+
"drag_to_select": "拖拽选择",
1024+
"clear_selection": "清除选择",
1025+
"choose_source_image_for_regional_editing": "选择源图片以进行区域编辑",
1026+
"choose_source_image_first": "请先选择源图片",
1027+
"select_edit_area_first": "请先选择要编辑的区域",
1028+
"describe_image_change_first": "请先描述图片修改内容"
10121029
}
10131030
}

0 commit comments

Comments
 (0)