diff --git a/README.md b/README.md index dcdeb6cb..f6155a45 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,10 @@ docker compose up -d MAX API 采用分层架构:应用、SDK 或 Agent 请求经统一入口进入,依次经过路由、中间件、控制器和业务服务层,最终由中继层适配到对应上游供应商;数据层和缓存层为模型治理、Agent 令牌治理、计费、日志、审计和任务状态提供持久化与加速能力。 +![MAX API 系统架构图](./docs/images/MAX-API架构图.png) + +以下简图展示核心请求链路: + ```mermaid flowchart LR Client[客户端 / SDK / Agent] --> Router[Router
路由] diff --git a/README.zh_CN.md b/README.zh_CN.md index e5aa6558..1b9e16d6 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -241,6 +241,10 @@ docker compose up -d MAX API 采用分层架构:应用、SDK 或 Agent 请求经统一入口进入,依次经过路由、中间件、控制器和业务服务层,最终由中继层适配到对应上游供应商;数据层和缓存层为模型治理、Agent 令牌治理、计费、日志、审计和任务状态提供持久化与加速能力。 +![MAX API 系统架构图](./docs/images/MAX-API架构图.png) + +以下简图展示核心请求链路: + ```mermaid flowchart LR Client[客户端 / SDK / Agent] --> Router[Router
路由] diff --git "a/docs/images/MAX-API\346\236\266\346\236\204\345\233\276.png" "b/docs/images/MAX-API\346\236\266\346\236\204\345\233\276.png" new file mode 100644 index 00000000..56dfd547 Binary files /dev/null and "b/docs/images/MAX-API\346\236\266\346\236\204\345\233\276.png" differ diff --git a/model/channel_update_fields_test.go b/model/channel_update_fields_test.go index df54753d..8d3de501 100644 --- a/model/channel_update_fields_test.go +++ b/model/channel_update_fields_test.go @@ -77,6 +77,63 @@ func installFailingAbilityDeleteTrigger(t *testing.T, db *gorm.DB) { t.Cleanup(func() { _ = db.Exec("DROP TRIGGER IF EXISTS fail_ability_delete").Error }) } +func populatedChannelForEditableUpdateMap() *Channel { + stringPtr := func(value string) *string { return &value } + priority := int64(1) + weight := uint(2) + autoBan := 1 + return &Channel{ + Type: 3, + Key: "key", + OpenAIOrganization: stringPtr("org"), + TestModel: stringPtr("gpt-test"), + Status: common.ChannelStatusEnabled, + Name: "schema-check-channel", + Weight: &weight, + BaseURL: stringPtr("https://example.com"), + Other: "{}", + Models: "gpt-test", + Group: "default", + ModelMapping: stringPtr("{}"), + StatusCodeMapping: stringPtr("{}"), + Priority: &priority, + AutoBan: &autoBan, + Tag: stringPtr("tag"), + Setting: stringPtr("{}"), + ParamOverride: stringPtr("{}"), + HeaderOverride: stringPtr("{}"), + Remark: stringPtr("remark"), + ChannelInfo: ChannelInfo{ + IsMultiKey: true, + MultiKeySize: 1, + MultiKeyPollingIndex: 0, + }, + OtherSettings: "{}", + } +} + +func TestEditableUpdateMapUsesOnlyChannelSchemaColumns(t *testing.T) { + db, _ := setupChannelUpdateFieldsTestDB(t) + + stmt := &gorm.Statement{DB: db} + require.NoError(t, stmt.Parse(&Channel{})) + + schemaColumns := make(map[string]struct{}, len(stmt.Schema.DBNames)) + for _, column := range stmt.Schema.DBNames { + schemaColumns[column] = struct{}{} + } + + channel := populatedChannelForEditableUpdateMap() + for _, field := range channelEditableUpdateFields { + updates := channel.editableUpdateMap([]string{field}) + require.NotEmpty(t, updates, "editable field %q is not handled by editableUpdateMap or the test fixture lacks its value", field) + for column := range updates { + require.Contains(t, schemaColumns, column, "editable field %q maps to unknown channel schema column %q", field, column) + require.True(t, db.Migrator().HasColumn(&Channel{}, column), "editable field %q maps to missing database column %q", field, column) + } + } +} + func TestUpdateFieldsSkipsAbilityRebuildForChannelInfoOnly(t *testing.T) { db, channel := setupChannelUpdateFieldsTestDB(t) installFailingAbilityDeleteTrigger(t, db)