diff --git a/devserver/server.go b/devserver/server.go index 1ca73c6..705cbce 100644 --- a/devserver/server.go +++ b/devserver/server.go @@ -188,7 +188,7 @@ func Run(cfg Config) error { // routePrefixes 从路由声明中提取不重复的路径前缀(如 /v1/) func routePrefixes(routes []sdk.RouteDefinition) []string { seen := make(map[string]bool) - var prefixes []string + prefixes := make([]string, 0, len(routes)) for _, r := range routes { // 取第二个 / 之前的部分作为前缀 parts := strings.SplitN(strings.TrimPrefix(r.Path, "/"), "/", 2) diff --git a/devserver/server_test.go b/devserver/server_test.go new file mode 100644 index 0000000..aea71c4 --- /dev/null +++ b/devserver/server_test.go @@ -0,0 +1,27 @@ +package devserver + +import ( + "testing" + + sdk "github.com/DouDOU-start/airgate-sdk" +) + +func TestRoutePrefixesDeduplicates(t *testing.T) { + t.Parallel() + + got := routePrefixes([]sdk.RouteDefinition{ + {Path: "/v1/chat/completions"}, + {Path: "/v1/models"}, + {Path: "/oauth/callback"}, + {Path: "/"}, + }) + want := []string{"/v1/", "/oauth/", "//"} + if len(got) != len(want) { + t.Fatalf("routePrefixes length = %d, want %d (%v)", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("routePrefixes[%d] = %q, want %q", i, got[i], want[i]) + } + } +} diff --git a/frontend/src/css.ts b/frontend/src/css.ts index e3150d8..93d0f1e 100644 --- a/frontend/src/css.ts +++ b/frontend/src/css.ts @@ -160,13 +160,23 @@ export function setTheme(theme: ThemeName, options: ThemeSetOptions = {}): void const themeAttribute = options.themeAttribute || 'data-theme'; const target = options.target || document.documentElement; target.setAttribute(themeAttribute, theme); - localStorage.setItem(options.storageKey || 'ag-theme', theme); + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(options.storageKey || 'ag-theme', theme); + } + } catch { + // Theme switching should keep working when storage is unavailable. + } } /** 读取已保存的主题偏好,默认 dark */ export function getStoredTheme(options: ThemeStorageOptions = {}): ThemeName { if (typeof localStorage === 'undefined') return 'dark'; - return (localStorage.getItem(options.storageKey || 'ag-theme') as ThemeName) || 'dark'; + try { + return (localStorage.getItem(options.storageKey || 'ag-theme') as ThemeName) || 'dark'; + } catch { + return 'dark'; + } } /** 生成 Tailwind 可消费的 theme bridge */ diff --git a/grpc/common.go b/grpc/common.go index 1af92eb..c800740 100644 --- a/grpc/common.go +++ b/grpc/common.go @@ -84,6 +84,9 @@ func (b *pluginBase) Info() sdk.PluginInfo { Dependencies: resp.Dependencies, } + if len(resp.ConfigSchema) > 0 { + info.ConfigSchema = make([]sdk.ConfigField, 0, len(resp.ConfigSchema)) + } for _, cf := range resp.ConfigSchema { info.ConfigSchema = append(info.ConfigSchema, sdk.ConfigField{ Key: cf.Key, @@ -96,12 +99,18 @@ func (b *pluginBase) Info() sdk.PluginInfo { }) } + if len(resp.AccountTypes) > 0 { + info.AccountTypes = make([]sdk.AccountType, 0, len(resp.AccountTypes)) + } for _, at := range resp.AccountTypes { accountType := sdk.AccountType{ Key: at.Key, Label: at.Label, Description: at.Description, } + if len(at.Fields) > 0 { + accountType.Fields = make([]sdk.CredentialField, 0, len(at.Fields)) + } for _, f := range at.Fields { accountType.Fields = append(accountType.Fields, sdk.CredentialField{ Key: f.Key, @@ -114,6 +123,9 @@ func (b *pluginBase) Info() sdk.PluginInfo { } info.AccountTypes = append(info.AccountTypes, accountType) } + if len(resp.FrontendPages) > 0 { + info.FrontendPages = make([]sdk.FrontendPage, 0, len(resp.FrontendPages)) + } for _, p := range resp.FrontendPages { info.FrontendPages = append(info.FrontendPages, sdk.FrontendPage{ Path: p.Path, @@ -123,6 +135,9 @@ func (b *pluginBase) Info() sdk.PluginInfo { Audience: p.Audience, }) } + if len(resp.FrontendWidgets) > 0 { + info.FrontendWidgets = make([]sdk.FrontendWidget, 0, len(resp.FrontendWidgets)) + } for _, w := range resp.FrontendWidgets { info.FrontendWidgets = append(info.FrontendWidgets, sdk.FrontendWidget{ Slot: w.Slot, diff --git a/grpc/extension_server.go b/grpc/extension_server.go index c301ccc..20be0b2 100644 --- a/grpc/extension_server.go +++ b/grpc/extension_server.go @@ -86,6 +86,9 @@ func (s *ExtensionGRPCServer) Migrate(_ context.Context, _ *pb.Empty) (*pb.Empty func (s *ExtensionGRPCServer) GetBackgroundTasks(_ context.Context, _ *pb.Empty) (*pb.BackgroundTasksResponse, error) { tasks := s.Impl.BackgroundTasks() resp := &pb.BackgroundTasksResponse{} + if len(tasks) > 0 { + resp.Tasks = make([]*pb.BackgroundTaskProto, 0, len(tasks)) + } taskMap := make(map[string]func(context.Context) error, len(tasks)) for _, t := range tasks { resp.Tasks = append(resp.Tasks, &pb.BackgroundTaskProto{ diff --git a/grpc/gateway_server.go b/grpc/gateway_server.go index 4c4d0e7..7b1ba4a 100644 --- a/grpc/gateway_server.go +++ b/grpc/gateway_server.go @@ -23,6 +23,9 @@ func (s *GatewayGRPCServer) GetPlatform(_ context.Context, _ *pb.Empty) (*pb.Str func (s *GatewayGRPCServer) GetModels(_ context.Context, _ *pb.Empty) (*pb.ModelsResponse, error) { models := s.Impl.Models() resp := &pb.ModelsResponse{} + if len(models) > 0 { + resp.Models = make([]*pb.ModelInfoProto, 0, len(models)) + } for _, m := range models { resp.Models = append(resp.Models, &pb.ModelInfoProto{ Id: m.ID, @@ -45,6 +48,9 @@ func (s *GatewayGRPCServer) GetModels(_ context.Context, _ *pb.Empty) (*pb.Model func (s *GatewayGRPCServer) GetRoutes(_ context.Context, _ *pb.Empty) (*pb.RoutesResponse, error) { routes := s.Impl.Routes() resp := &pb.RoutesResponse{} + if len(routes) > 0 { + resp.Routes = make([]*pb.RouteDefinitionProto, 0, len(routes)) + } for _, r := range routes { resp.Routes = append(resp.Routes, &pb.RouteDefinitionProto{ Method: r.Method, diff --git a/grpc/plugin_server.go b/grpc/plugin_server.go index 94f23be..66b8dbb 100644 --- a/grpc/plugin_server.go +++ b/grpc/plugin_server.go @@ -35,6 +35,9 @@ func (s *PluginGRPCServer) GetInfo(_ context.Context, _ *pb.Empty) (*pb.PluginIn Dependencies: info.Dependencies, } + if len(info.ConfigSchema) > 0 { + resp.ConfigSchema = make([]*pb.ConfigFieldProto, 0, len(info.ConfigSchema)) + } for _, cf := range info.ConfigSchema { resp.ConfigSchema = append(resp.ConfigSchema, &pb.ConfigFieldProto{ Key: cf.Key, @@ -47,12 +50,18 @@ func (s *PluginGRPCServer) GetInfo(_ context.Context, _ *pb.Empty) (*pb.PluginIn }) } + if len(info.AccountTypes) > 0 { + resp.AccountTypes = make([]*pb.AccountTypeProto, 0, len(info.AccountTypes)) + } for _, at := range info.AccountTypes { atProto := &pb.AccountTypeProto{ Key: at.Key, Label: at.Label, Description: at.Description, } + if len(at.Fields) > 0 { + atProto.Fields = make([]*pb.CredentialFieldProto, 0, len(at.Fields)) + } for _, f := range at.Fields { atProto.Fields = append(atProto.Fields, &pb.CredentialFieldProto{ Key: f.Key, @@ -65,6 +74,9 @@ func (s *PluginGRPCServer) GetInfo(_ context.Context, _ *pb.Empty) (*pb.PluginIn } resp.AccountTypes = append(resp.AccountTypes, atProto) } + if len(info.FrontendPages) > 0 { + resp.FrontendPages = make([]*pb.FrontendPageProto, 0, len(info.FrontendPages)) + } for _, p := range info.FrontendPages { resp.FrontendPages = append(resp.FrontendPages, &pb.FrontendPageProto{ Path: p.Path, @@ -74,6 +86,9 @@ func (s *PluginGRPCServer) GetInfo(_ context.Context, _ *pb.Empty) (*pb.PluginIn Audience: p.Audience, }) } + if len(info.FrontendWidgets) > 0 { + resp.FrontendWidgets = make([]*pb.FrontendWidgetProto, 0, len(info.FrontendWidgets)) + } for _, w := range info.FrontendWidgets { resp.FrontendWidgets = append(resp.FrontendWidgets, &pb.FrontendWidgetProto{ Slot: w.Slot, @@ -166,7 +181,7 @@ func (s *PluginGRPCServer) GetWebAssets(_ context.Context, _ *pb.Empty) (*pb.Web if len(assets) == 0 { return &pb.WebAssetsResponse{HasAssets: false}, nil } - resp := &pb.WebAssetsResponse{HasAssets: true} + resp := &pb.WebAssetsResponse{HasAssets: true, Files: make([]*pb.WebAssetFile, 0, len(assets))} for path, content := range assets { resp.Files = append(resp.Files, &pb.WebAssetFile{ Path: path,