Merge pull request #5094 from wucm667/feat/issue-5065-compact-homepage

feat(home): add compact home page preset to avoid abuse classification
This commit is contained in:
Wesley Liddick authored and GitHub committed 2026-07-31 21:51:33 +08:00
commit 2980ff3850
26 files changed
+300 -1

No files matched your search

@@ -233,6 +233,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
ContactInfo: settings.ContactInfo, ContactInfo: settings.ContactInfo,
DocURL: settings.DocURL, DocURL: settings.DocURL,
HomeContent: settings.HomeContent, HomeContent: settings.HomeContent,
CompactHomeEnabled: settings.CompactHomeEnabled,
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
@@ -299,6 +299,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.HomeContent != after.HomeContent { if before.HomeContent != after.HomeContent {
changed = append(changed, "home_content") changed = append(changed, "home_content")
} }
if before.CompactHomeEnabled != after.CompactHomeEnabled {
changed = append(changed, "compact_home_enabled")
}
if before.HideCcsImportButton != after.HideCcsImportButton { if before.HideCcsImportButton != after.HideCcsImportButton {
changed = append(changed, "hide_ccs_import_button") changed = append(changed, "hide_ccs_import_button")
} }
@@ -64,6 +64,18 @@ func TestDiffSettings_NoChangeWhenEqual(t *testing.T) {
} }
} }
func TestDiffSettings_DetectsCompactHomeChange(t *testing.T) {
changed := diffSettings(
&service.SystemSettings{},
&service.SystemSettings{CompactHomeEnabled: true},
nil,
nil,
UpdateSettingsRequest{},
)
require.Contains(t, changed, service.SettingKeyCompactHomeEnabled)
}
func TestEqualNullableFloat(t *testing.T) { func TestEqualNullableFloat(t *testing.T) {
five := 5.0 five := 5.0
five2 := 5.0 five2 := 5.0
@@ -142,6 +142,7 @@ type UpdateSettingsRequest struct {
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
CompactHomeEnabled bool `json:"compact_home_enabled"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"` PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
@@ -1430,6 +1431,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
ContactInfo: req.ContactInfo, ContactInfo: req.ContactInfo,
DocURL: req.DocURL, DocURL: req.DocURL,
HomeContent: req.HomeContent, HomeContent: req.HomeContent,
CompactHomeEnabled: req.CompactHomeEnabled,
HideCcsImportButton: req.HideCcsImportButton, HideCcsImportButton: req.HideCcsImportButton,
PurchaseSubscriptionEnabled: purchaseEnabled, PurchaseSubscriptionEnabled: purchaseEnabled,
PurchaseSubscriptionURL: purchaseURL, PurchaseSubscriptionURL: purchaseURL,
@@ -1982,6 +1984,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
ContactInfo: updatedSettings.ContactInfo, ContactInfo: updatedSettings.ContactInfo,
DocURL: updatedSettings.DocURL, DocURL: updatedSettings.DocURL,
HomeContent: updatedSettings.HomeContent, HomeContent: updatedSettings.HomeContent,
CompactHomeEnabled: updatedSettings.CompactHomeEnabled,
HideCcsImportButton: updatedSettings.HideCcsImportButton, HideCcsImportButton: updatedSettings.HideCcsImportButton,
PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
+2
View File
@@ -142,6 +142,7 @@ type SystemSettings struct {
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
CompactHomeEnabled bool `json:"compact_home_enabled"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
@@ -344,6 +345,7 @@ type PublicSettings struct {
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
CompactHomeEnabled bool `json:"compact_home_enabled"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
@@ -67,6 +67,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
ContactInfo: settings.ContactInfo, ContactInfo: settings.ContactInfo,
DocURL: settings.DocURL, DocURL: settings.DocURL,
HomeContent: settings.HomeContent, HomeContent: settings.HomeContent,
CompactHomeEnabled: settings.CompactHomeEnabled,
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
@@ -874,6 +874,7 @@ func TestAPIContracts(t *testing.T) {
"max_codex_version": "", "max_codex_version": "",
"codex_cli_only_blacklist": "", "codex_cli_only_blacklist": "",
"codex_cli_only_whitelist": "", "codex_cli_only_whitelist": "",
"compact_home_enabled": false,
"codex_cli_only_allow_app_server_clients": false, "codex_cli_only_allow_app_server_clients": false,
"codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]", "codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]",
"allow_ungrouped_key_scheduling": false, "allow_ungrouped_key_scheduling": false,
@@ -1175,6 +1176,7 @@ func TestAPIContracts(t *testing.T) {
"max_codex_version": "", "max_codex_version": "",
"codex_cli_only_blacklist": "", "codex_cli_only_blacklist": "",
"codex_cli_only_whitelist": "", "codex_cli_only_whitelist": "",
"compact_home_enabled": false,
"codex_cli_only_allow_app_server_clients": false, "codex_cli_only_allow_app_server_clients": false,
"codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]", "codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]",
"web_search_emulation_enabled": false, "web_search_emulation_enabled": false,
@@ -271,6 +271,7 @@ const (
SettingKeyContactInfo = "contact_info" // 客服联系方式 SettingKeyContactInfo = "contact_info" // 客服联系方式
SettingKeyDocURL = "doc_url" // 文档链接 SettingKeyDocURL = "doc_url" // 文档链接
SettingKeyHomeContent = "home_content" // 首页内容(支持 Markdown/HTML,或 URL 作为 iframe src) SettingKeyHomeContent = "home_content" // 首页内容(支持 Markdown/HTML,或 URL 作为 iframe src)
SettingKeyCompactHomeEnabled = "compact_home_enabled" // 是否启用内置简洁首页
SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮 SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口 SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src) SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src)
@@ -330,6 +330,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
ContactInfo: settings[SettingKeyContactInfo], ContactInfo: settings[SettingKeyContactInfo],
DocURL: settings[SettingKeyDocURL], DocURL: settings[SettingKeyDocURL],
HomeContent: settings[SettingKeyHomeContent], HomeContent: settings[SettingKeyHomeContent],
CompactHomeEnabled: settings[SettingKeyCompactHomeEnabled] == "true",
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true", HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true", PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]), PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
@@ -179,6 +179,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyContactInfo, SettingKeyContactInfo,
SettingKeyDocURL, SettingKeyDocURL,
SettingKeyHomeContent, SettingKeyHomeContent,
SettingKeyCompactHomeEnabled,
SettingKeyHideCcsImportButton, SettingKeyHideCcsImportButton,
SettingKeyPurchaseSubscriptionEnabled, SettingKeyPurchaseSubscriptionEnabled,
SettingKeyPurchaseSubscriptionURL, SettingKeyPurchaseSubscriptionURL,
@@ -307,6 +308,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
ContactInfo: settings[SettingKeyContactInfo], ContactInfo: settings[SettingKeyContactInfo],
DocURL: settings[SettingKeyDocURL], DocURL: settings[SettingKeyDocURL],
HomeContent: settings[SettingKeyHomeContent], HomeContent: settings[SettingKeyHomeContent],
CompactHomeEnabled: settings[SettingKeyCompactHomeEnabled] == "true",
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true", HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true", PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]), PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
@@ -495,6 +497,7 @@ type PublicSettingsInjectionPayload struct {
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
CompactHomeEnabled bool `json:"compact_home_enabled"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
@@ -567,6 +570,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
ContactInfo: settings.ContactInfo, ContactInfo: settings.ContactInfo,
DocURL: settings.DocURL, DocURL: settings.DocURL,
HomeContent: settings.HomeContent, HomeContent: settings.HomeContent,
CompactHomeEnabled: settings.CompactHomeEnabled,
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
@@ -78,6 +78,25 @@ func TestSettingService_GetPublicSettings_ExposesTablePreferences(t *testing.T)
require.Equal(t, []int{20, 50, 100}, settings.TablePageSizeOptions) require.Equal(t, []int{20, 50, 100}, settings.TablePageSizeOptions)
} }
func TestSettingService_GetPublicSettings_ExposesCompactHomeEnabled(t *testing.T) {
repo := &settingPublicRepoStub{
values: map[string]string{
SettingKeyCompactHomeEnabled: "true",
},
}
svc := NewSettingService(repo, &config.Config{})
settings, err := svc.GetPublicSettings(context.Background())
require.NoError(t, err)
require.True(t, settings.CompactHomeEnabled)
missingSettings, err := NewSettingService(&settingPublicRepoStub{values: map[string]string{}}, &config.Config{}).
GetPublicSettings(context.Background())
require.NoError(t, err)
require.False(t, missingSettings.CompactHomeEnabled)
}
func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t *testing.T) { func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t *testing.T) {
repo := &settingPublicRepoStub{ repo := &settingPublicRepoStub{
values: map[string]string{ values: map[string]string{
@@ -228,6 +228,16 @@ func (s *defaultSubGroupReaderStub) GetByID(ctx context.Context, id int64) (*Gro
return nil, ErrGroupNotFound return nil, ErrGroupNotFound
} }
func TestSettingService_UpdateSettings_PersistsCompactHomeEnabled(t *testing.T) {
repo := &settingUpdateRepoStub{}
svc := NewSettingService(repo, &config.Config{})
err := svc.UpdateSettings(context.Background(), &SystemSettings{CompactHomeEnabled: true})
require.NoError(t, err)
require.Equal(t, "true", repo.updates[SettingKeyCompactHomeEnabled])
}
func TestSettingService_UpdateSettings_DefaultSubscriptions_ValidGroup(t *testing.T) { func TestSettingService_UpdateSettings_DefaultSubscriptions_ValidGroup(t *testing.T) {
repo := &settingUpdateRepoStub{} repo := &settingUpdateRepoStub{}
groupReader := &defaultSubGroupReaderStub{ groupReader := &defaultSubGroupReaderStub{
@@ -320,6 +320,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeyContactInfo] = settings.ContactInfo updates[SettingKeyContactInfo] = settings.ContactInfo
updates[SettingKeyDocURL] = settings.DocURL updates[SettingKeyDocURL] = settings.DocURL
updates[SettingKeyHomeContent] = settings.HomeContent updates[SettingKeyHomeContent] = settings.HomeContent
updates[SettingKeyCompactHomeEnabled] = strconv.FormatBool(settings.CompactHomeEnabled)
updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton) updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled) updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled)
updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL) updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
@@ -139,6 +139,7 @@ type SystemSettings struct {
ContactInfo string ContactInfo string
DocURL string DocURL string
HomeContent string HomeContent string
CompactHomeEnabled bool
HideCcsImportButton bool HideCcsImportButton bool
PurchaseSubscriptionEnabled bool PurchaseSubscriptionEnabled bool
PurchaseSubscriptionURL string PurchaseSubscriptionURL string
@@ -305,6 +306,7 @@ type PublicSettings struct {
ContactInfo string ContactInfo string
DocURL string DocURL string
HomeContent string HomeContent string
CompactHomeEnabled bool
HideCcsImportButton bool HideCcsImportButton bool
PurchaseSubscriptionEnabled bool PurchaseSubscriptionEnabled bool
+2
View File
@@ -440,6 +440,7 @@ export interface SystemSettings {
contact_info: string; contact_info: string;
doc_url: string; doc_url: string;
home_content: string; home_content: string;
compact_home_enabled: boolean;
hide_ccs_import_button: boolean; hide_ccs_import_button: boolean;
table_default_page_size: number; table_default_page_size: number;
table_page_size_options: number[]; table_page_size_options: number[];
@@ -752,6 +753,7 @@ export interface UpdateSettingsRequest {
contact_info?: string; contact_info?: string;
doc_url?: string; doc_url?: string;
home_content?: string; home_content?: string;
compact_home_enabled?: boolean;
hide_ccs_import_button?: boolean; hide_ccs_import_button?: boolean;
table_default_page_size?: number; table_default_page_size?: number;
table_page_size_options?: number[]; table_page_size_options?: number[];
@@ -73,6 +73,7 @@ function buildPublicSettings(overrides: Partial<WeChatPublicSettings> = {}): WeC
contact_info: '', contact_info: '',
doc_url: '', doc_url: '',
home_content: '', home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
payment_enabled: false, payment_enabled: false,
table_default_page_size: 20, table_default_page_size: 20,
@@ -250,6 +250,7 @@ describe('ProfileIdentityBindingsSection', () => {
contact_info: '', contact_info: '',
doc_url: '', doc_url: '',
home_content: '', home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
payment_enabled: false, payment_enabled: false,
table_default_page_size: 20, table_default_page_size: 20,
@@ -549,6 +549,8 @@ export default {
homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.', homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.',
homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.', homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.',
homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.', homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.',
compactHome: 'Compact Home Page',
compactHomeHint: 'Show a restrained site identity page when no custom home page content is set.',
hideCcsImportButton: 'Hide CCS Import Button', hideCcsImportButton: 'Hide CCS Import Button',
hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page'
}, },
@@ -544,6 +544,8 @@ export default {
'自定义首页内容,支持 Markdown/HTML。如果输入的是链接(以 http:// 或 https:// 开头),则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。设置后首页的状态信息将不再显示。', '自定义首页内容,支持 Markdown/HTML。如果输入的是链接(以 http:// 或 https:// 开头),则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。设置后首页的状态信息将不再显示。',
homeContentIframeWarning: homeContentIframeWarning:
'⚠️ iframe 模式提示:部分网站设置了 X-Frame-Options 或 CSP 安全策略,禁止被嵌入到 iframe 中。如果页面显示空白或报错,请确认目标网站允许被嵌入,或考虑使用 HTML 模式自行构建页面内容。', '⚠️ iframe 模式提示:部分网站设置了 X-Frame-Options 或 CSP 安全策略,禁止被嵌入到 iframe 中。如果页面显示空白或报错,请确认目标网站允许被嵌入,或考虑使用 HTML 模式自行构建页面内容。',
compactHome: '简洁首页',
compactHomeHint: '未设置自定义首页内容时,展示简洁的站点信息页面。',
hideCcsImportButton: '隐藏 CCS 导入按钮', hideCcsImportButton: '隐藏 CCS 导入按钮',
hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮'
}, },
@@ -33,6 +33,7 @@ function createPublicSettings(overrides: Partial<PublicSettings> = {}): PublicSe
contact_info: '', contact_info: '',
doc_url: '', doc_url: '',
home_content: '', home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
payment_enabled: false, payment_enabled: false,
risk_control_enabled: false, risk_control_enabled: false,
@@ -452,6 +453,7 @@ describe('useAppStore', () => {
contact_info: '', contact_info: '',
doc_url: '', doc_url: '',
home_content: '', home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
purchase_subscription_enabled: false, purchase_subscription_enabled: false,
purchase_subscription_url: '', purchase_subscription_url: '',
+1
View File
@@ -342,6 +342,7 @@ export const useAppStore = defineStore('app', () => {
contact_info: contactInfo.value, contact_info: contactInfo.value,
doc_url: docUrl.value, doc_url: docUrl.value,
home_content: '', home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
payment_enabled: false, payment_enabled: false,
table_default_page_size: 20, table_default_page_size: 20,
+1
View File
@@ -210,6 +210,7 @@ export interface PublicSettings {
contact_info: string contact_info: string
doc_url: string doc_url: string
home_content: string home_content: string
compact_home_enabled: boolean
hide_ccs_import_button: boolean hide_ccs_import_button: boolean
payment_enabled: boolean payment_enabled: boolean
risk_control_enabled: boolean risk_control_enabled: boolean
+72 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<!-- Custom Home Content: Full Page Mode --> <!-- Custom Home Content: Full Page Mode -->
<div v-if="homeContent" class="min-h-screen"> <div v-if="hasHomeContent" class="min-h-screen">
<!-- iframe mode --> <!-- iframe mode -->
<iframe <iframe
v-if="isHomeContentUrl" v-if="isHomeContentUrl"
@@ -12,6 +12,75 @@
<div v-else v-html="homeContent"></div> <div v-else v-html="homeContent"></div>
</div> </div>
<!-- Compact Home Page -->
<div
v-else-if="compactHomeEnabled"
data-testid="compact-home"
class="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-dark-950 dark:text-white"
>
<header class="border-b border-gray-200 px-4 py-4 sm:px-6 dark:border-dark-800">
<nav class="mx-auto flex max-w-5xl flex-wrap items-center justify-between gap-3 sm:gap-4">
<div class="flex min-w-0 flex-1 items-center gap-3">
<img
:src="siteLogo || '/logo.svg'"
alt="Logo"
class="h-9 w-9 shrink-0 rounded-lg object-contain"
/>
<span class="min-w-0 truncate text-base font-semibold">{{ siteName }}</span>
</div>
<div class="flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2">
<LocaleSwitcher />
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 dark:text-dark-400 dark:hover:bg-dark-800"
:title="t('home.viewDocs')"
>
<Icon name="book" size="md" />
</a>
<button
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 dark:text-dark-400 dark:hover:bg-dark-800"
:title="isDark ? t('home.switchToLight') : t('home.switchToDark')"
@click="toggleTheme"
>
<Icon v-if="isDark" name="sun" size="md" />
<Icon v-else name="moon" size="md" />
</button>
<router-link
:to="isAuthenticated ? dashboardPath : '/login'"
class="inline-flex min-h-10 shrink-0 items-center justify-center rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200"
>
{{ isAuthenticated ? t('home.dashboard') : t('home.login') }}
</router-link>
</div>
</nav>
</header>
<main class="flex min-w-0 flex-1 items-center justify-center px-4 py-16 sm:px-6">
<div class="min-w-0 max-w-2xl text-center">
<img
:src="siteLogo || '/logo.svg'"
alt="Logo"
class="mx-auto mb-6 h-20 w-20 rounded-2xl object-contain"
/>
<h1 class="[overflow-wrap:anywhere] text-3xl font-bold md:text-4xl">{{ siteName }}</h1>
<p class="mt-4 whitespace-pre-wrap [overflow-wrap:anywhere] text-base text-gray-600 dark:text-dark-300">{{ siteSubtitle }}</p>
<router-link
:to="isAuthenticated ? dashboardPath : '/login'"
class="mt-8 inline-flex min-h-10 items-center justify-center rounded-lg bg-primary-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-700"
>
{{ isAuthenticated ? t('home.goToDashboard') : t('home.login') }}
</router-link>
</div>
</main>
<footer class="min-w-0 border-t border-gray-200 px-4 py-5 text-center text-sm text-gray-500 [overflow-wrap:anywhere] sm:px-6 dark:border-dark-800 dark:text-dark-400">
&copy; {{ currentYear }} {{ siteName }}
</footer>
</div>
<!-- Default Home Page --> <!-- Default Home Page -->
<div <div
v-else v-else
@@ -423,6 +492,8 @@ const siteLogo = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.site_
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform') const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform')
const docUrl = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')) const docUrl = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.doc_url || appStore.docUrl || ''))
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '') const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
const hasHomeContent = computed(() => homeContent.value.trim().length > 0)
const compactHomeEnabled = computed(() => appStore.cachedPublicSettings?.compact_home_enabled === true)
// Check if homeContent is a URL (for iframe display) // Check if homeContent is a URL (for iframe display)
const isHomeContentUrl = computed(() => { const isHomeContentUrl = computed(() => {
@@ -0,0 +1,122 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, RouterLinkStub } from '@vue/test-utils'
import HomeView from '../HomeView.vue'
const { appStore, authStore } = vi.hoisted(() => ({
appStore: {
cachedPublicSettings: {} as Record<string, unknown>,
siteName: 'Fallback site',
siteLogo: '',
docUrl: '',
publicSettingsLoaded: true,
fetchPublicSettings: vi.fn(),
},
authStore: {
isAuthenticated: false,
isAdmin: false,
user: null as { email?: string } | null,
checkAuth: vi.fn(),
},
}))
vi.mock('@/stores', () => ({
useAppStore: () => appStore,
useAuthStore: () => authStore,
}))
vi.mock('vue-i18n', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-i18n')>()
return {
...actual,
useI18n: () => ({ t: (key: string) => key }),
}
})
function mountHome(settings: Record<string, unknown> = {}) {
appStore.cachedPublicSettings = {
site_name: 'Test site',
site_subtitle: 'Test subtitle',
...settings,
}
return mount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,
LocaleSwitcher: { template: '<div data-testid="locale-switcher" />' },
Icon: { template: '<span data-testid="icon" />' },
},
},
})
}
function compactDestination(wrapper: ReturnType<typeof mountHome>) {
return wrapper.get('[data-testid="compact-home"]').findComponent(RouterLinkStub).props('to')
}
describe('HomeView compact mode', () => {
beforeEach(() => {
authStore.isAuthenticated = false
authStore.isAdmin = false
authStore.user = null
authStore.checkAuth.mockClear()
appStore.fetchPublicSettings.mockClear()
localStorage.clear()
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: false } as MediaQueryList)
})
it('renders custom HTML ahead of compact mode', () => {
const wrapper = mountHome({
compact_home_enabled: true,
home_content: '<section id="custom-home">Custom home</section>',
})
expect(wrapper.get('#custom-home').text()).toBe('Custom home')
expect(wrapper.find('[data-testid="compact-home"]').exists()).toBe(false)
})
it('renders custom URL content ahead of compact mode', () => {
const wrapper = mountHome({
compact_home_enabled: true,
home_content: ' https://example.com/home ',
})
expect(wrapper.get('iframe').attributes('src')).toBe('https://example.com/home')
expect(wrapper.find('[data-testid="compact-home"]').exists()).toBe(false)
})
it('treats whitespace-only custom content as empty and selects compact mode', () => {
const wrapper = mountHome({ compact_home_enabled: true, home_content: ' \n\t ' })
expect(wrapper.get('[data-testid="compact-home"]').text()).toContain('Test site')
})
it.each([undefined, false])('selects the default home when compact mode is %s', (enabled) => {
const settings = enabled === undefined ? {} : { compact_home_enabled: enabled }
const wrapper = mountHome(settings)
expect(wrapper.find('[data-testid="compact-home"]').exists()).toBe(false)
expect(wrapper.find('.terminal-container').exists()).toBe(true)
})
it('links unauthenticated visitors to login', () => {
expect(compactDestination(mountHome({ compact_home_enabled: true }))).toBe('/login')
})
it('links authenticated users to their dashboard', () => {
authStore.isAuthenticated = true
expect(compactDestination(mountHome({ compact_home_enabled: true }))).toBe('/dashboard')
})
it('links administrators to the admin dashboard', () => {
authStore.isAuthenticated = true
authStore.isAdmin = true
const wrapper = mountHome({ compact_home_enabled: true })
expect(compactDestination(wrapper)).toBe('/admin/dashboard')
expect(authStore.checkAuth).toHaveBeenCalledOnce()
expect(appStore.fetchPublicSettings).not.toHaveBeenCalled()
})
})
+15
View File
@@ -5977,6 +5977,19 @@
</p> </p>
</div> </div>
<!-- Compact Home Page -->
<div class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.site.compactHome")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.site.compactHomeHint") }}
</p>
</div>
<Toggle v-model="form.compact_home_enabled" data-testid="compact-home-toggle" />
</div>
<!-- Hide CCS Import Button --> <!-- Hide CCS Import Button -->
<div <div
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700" class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
@@ -8837,6 +8850,7 @@ const form = reactive<SettingsForm>({
contact_info: "", contact_info: "",
doc_url: "", doc_url: "",
home_content: "", home_content: "",
compact_home_enabled: false,
backend_mode_enabled: false, backend_mode_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
payment_enabled: false, payment_enabled: false,
@@ -10361,6 +10375,7 @@ async function saveSettings() {
contact_info: form.contact_info, contact_info: form.contact_info,
doc_url: form.doc_url, doc_url: form.doc_url,
home_content: form.home_content, home_content: form.home_content,
compact_home_enabled: form.compact_home_enabled,
backend_mode_enabled: form.backend_mode_enabled, backend_mode_enabled: form.backend_mode_enabled,
hide_ccs_import_button: form.hide_ccs_import_button, hide_ccs_import_button: form.hide_ccs_import_button,
table_default_page_size: form.table_default_page_size, table_default_page_size: form.table_default_page_size,
@@ -378,6 +378,7 @@ const baseSettingsResponse = {
contact_info: "", contact_info: "",
doc_url: "", doc_url: "",
home_content: "", home_content: "",
compact_home_enabled: false,
hide_ccs_import_button: false, hide_ccs_import_button: false,
table_default_page_size: 20, table_default_page_size: 20,
table_page_size_options: [10, 20, 50, 100], table_page_size_options: [10, 20, 50, 100],
@@ -680,6 +681,22 @@ describe("admin SettingsView payment visible method controls", () => {
adminSettingsFetch.mockResolvedValue(undefined); adminSettingsFetch.mockResolvedValue(undefined);
}); });
it("submits the compact home page toggle", async () => {
const wrapper = mountView();
await flushPromises();
const toggle = wrapper.get('[data-testid="compact-home-toggle"]');
expect((toggle.element as HTMLInputElement).checked).toBe(false);
await toggle.setValue(true);
await wrapper.find("form").trigger("submit.prevent");
await flushPromises();
expect(updateSettings).toHaveBeenCalledWith(
expect.objectContaining({ compact_home_enabled: true }),
);
});
it("renders panel rate limit card and saves settings", async () => { it("renders panel rate limit card and saves settings", async () => {
getPanelRateLimitSettings.mockClear(); getPanelRateLimitSettings.mockClear();
updatePanelRateLimitSettings.mockClear(); updatePanelRateLimitSettings.mockClear();