From f06ead40ddea573d704d07bcb41cb4e95dd71d65 Mon Sep 17 00:00:00 2001 From: O Date: Wed, 16 Sep 2026 10:03:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=8C=83=E5=9B=B4=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=20(#660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 优化日期时间范围选择器 * feat(web): 添加主页智能模型选择 --- internal/control/home.go | 38 +- internal/control/home_test.go | 7 + web/package.json | 1 + web/pnpm-lock.yaml | 3 + web/src/app/resources/home.ts | 6 +- web/src/components/ui/AppCombobox.vue | 307 ++++++++++ .../components/ui/AppDateTimeRangePicker.vue | 539 ++++++++++++++++-- web/src/features/home/GatewayConnection.vue | 32 +- web/src/features/home/gateway-clients.ts | 42 ++ web/src/i18n/locales/en-US/core.ts | 3 +- web/src/i18n/locales/en-US/monitor.ts | 3 + web/src/i18n/locales/ja-JP/core.ts | 3 +- web/src/i18n/locales/ja-JP/monitor.ts | 3 + web/src/i18n/locales/zh-CN/core.ts | 3 +- web/src/i18n/locales/zh-CN/monitor.ts | 3 + 15 files changed, 922 insertions(+), 71 deletions(-) create mode 100644 web/src/components/ui/AppCombobox.vue diff --git a/internal/control/home.go b/internal/control/home.go index d95e7b0b..74acec3a 100644 --- a/internal/control/home.go +++ b/internal/control/home.go @@ -3,6 +3,7 @@ package control import ( "context" "fmt" + "sort" "strings" "time" @@ -30,6 +31,7 @@ type HomeAccessKey struct { Name string `json:"name"` MaskedKey string `json:"masked_key"` Protocols []protocol.Protocol `json:"protocols"` + Models []string `json:"models"` } type HomeBase struct { @@ -179,7 +181,7 @@ func (s *Service) readHomeBase( return HomeBase{}, app_errors.ErrUnauthorized } } - accessKeys, err := mapHomeAccessKeys(accessKeyRows) + accessKeys, err := mapHomeAccessKeys(accessKeyRows, snapshot) if err != nil { return HomeBase{}, err } @@ -338,6 +340,14 @@ func countScopedHomeModels( allowedGroups map[uint]struct{}, accessKey state.AccessKeyView, ) int64 { + return int64(len(scopedHomeModelNames(snapshot, allowedGroups, accessKey))) +} + +func scopedHomeModelNames( + snapshot *state.ConfigSnapshot, + allowedGroups map[uint]struct{}, + accessKey state.AccessKeyView, +) []string { names := make(map[string]struct{}) for groupID := range allowedGroups { group, exists := snapshot.Groups[groupID] @@ -357,7 +367,12 @@ func countScopedHomeModels( names[name] = struct{}{} } } - return int64(len(names)) + result := make([]string, 0, len(names)) + for name := range names { + result = append(result, name) + } + sort.Strings(result) + return result } func homeExternalModelName(model state.ModelConfig) string { @@ -486,7 +501,10 @@ func countAvailableHomeCredentialsInGroups( return available, nil } -func mapHomeAccessKeys(rows []homeAccessKeyRow) ([]HomeAccessKey, error) { +func mapHomeAccessKeys( + rows []homeAccessKeyRow, + snapshot *state.ConfigSnapshot, +) ([]HomeAccessKey, error) { result := make([]HomeAccessKey, 0, len(rows)) for _, row := range rows { if uint64(row.ID) > uint64(maxSafeInteger) { @@ -517,10 +535,24 @@ func mapHomeAccessKeys(rows []homeAccessKeyRow) ([]HomeAccessKey, error) { } else { protocols = append([]protocol.Protocol(nil), protocols...) } + accessKey, exists := snapshot.AccessKeysByID[row.ID] + if !exists || accessKey.Status != state.AccessKeyStatusActive { + return nil, fmt.Errorf( + "map home access key %d: runtime configuration mismatch: %w", + row.ID, + app_errors.ErrInternalServer, + ) + } + models := scopedHomeModelNames( + snapshot, + accessibleHomeGroups(snapshot, accessKey), + accessKey, + ) result = append(result, HomeAccessKey{ ID: row.ID, Name: row.Name, MaskedKey: maskedAccessKey(row.KeyPrefix, row.KeySuffix), Protocols: protocols, + Models: models, }) } return result, nil diff --git a/internal/control/home_test.go b/internal/control/home_test.go index 3cc3b414..ef53fcf5 100644 --- a/internal/control/home_test.go +++ b/internal/control/home_test.go @@ -161,11 +161,17 @@ func TestReadHomeBaseUsesPersistedAndRuntimeSnapshots(t *testing.T) { protocol.OpenAICompletions, protocol.Gemini, }, + Models: []string{ + "client-primary", "client-secondary", "client-third", "ignored-empty", + }, }, { ID: 9, Name: "all protocols", MaskedKey: "sk-gl-****c0de", Protocols: protocol.DataPlaneProtocols(), + Models: []string{ + "client-primary", "client-secondary", "client-third", "ignored-empty", + }, }, }, } @@ -236,6 +242,7 @@ func TestReadAccessKeyHomeBaseScopesInventoryToRoutableModels(t *testing.T) { } if result.Inventory != (HomeInventory{GroupCount: 1, ModelCount: 1}) || len(result.AccessKeys) != 1 || result.AccessKeys[0].ID != created.ID || + !reflect.DeepEqual(result.AccessKeys[0].Models, []string{"client-allowed"}) || result.CurrentAccessKey == nil || result.CurrentAccessKey.ID != created.ID { t.Fatalf("ReadAccessKeyHomeBase() = %#v", result) } diff --git a/web/package.json b/web/package.json index 36ffd13a..0c98877f 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,7 @@ "type-check": "vue-tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json" }, "dependencies": { + "@internationalized/date": "3.12.3", "@lucide/vue": "1.39.0", "@tanstack/vue-query": "5.102.8", "reka-ui": "2.10.4", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 60969640..d9dfe93c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: .: dependencies: + '@internationalized/date': + specifier: 3.12.3 + version: 3.12.3 '@lucide/vue': specifier: 1.39.0 version: 1.39.0(vue@3.5.42(typescript@6.0.3)) diff --git a/web/src/app/resources/home.ts b/web/src/app/resources/home.ts index de08b747..ad2ca25f 100644 --- a/web/src/app/resources/home.ts +++ b/web/src/app/resources/home.ts @@ -45,6 +45,7 @@ export interface HomeBaseDto { name: string masked_key: string protocols: AccessProtocol[] + models: string[] }> current_access_key: AccessKeyCollectionItemDto | null } @@ -145,7 +146,7 @@ const inventoryFields = [ 'available_credential_count', 'model_count', ] as const -const accessKeyFields = ['id', 'name', 'masked_key', 'protocols'] as const +const accessKeyFields = ['id', 'name', 'masked_key', 'protocols', 'models'] as const const statisticsFields = [ 'range', 'granularity', @@ -236,12 +237,15 @@ function projectHomeAccessKey(value: unknown): HomeBaseDto['access_keys'][number const protocols = projectArray(record.protocols, (protocol) => projectEnum(protocol, knownAccessProtocols), ) + const models = projectArray(record.models, projectNonBlankTrimmedString) if (new Set(protocols).size !== protocols.length) invalidResponse() + if (new Set(models).size !== models.length) invalidResponse() return { id: projectSafeInteger(record.id, { minimum: 1 }), name: projectNonBlankTrimmedString(record.name), masked_key: maskedKey, protocols, + models, } } diff --git a/web/src/components/ui/AppCombobox.vue b/web/src/components/ui/AppCombobox.vue new file mode 100644 index 00000000..f1c046fc --- /dev/null +++ b/web/src/components/ui/AppCombobox.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/web/src/components/ui/AppDateTimeRangePicker.vue b/web/src/components/ui/AppDateTimeRangePicker.vue index 0a027b13..60648a96 100644 --- a/web/src/components/ui/AppDateTimeRangePicker.vue +++ b/web/src/components/ui/AppDateTimeRangePicker.vue @@ -1,7 +1,25 @@