diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 6735029c7c3..2030ddf0510 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -3105,6 +3105,7 @@ export default {
expiresAt: 'Expires At',
actions: 'Actions'
},
+ usageWindowsHint: '"5h / 7d" are the upstream account\'s official rolling usage windows (e.g. OpenAI ChatGPT, Claude). They are imposed by the upstream provider on the account itself — not configured by sub2api, and unrelated to the models you map. Usage resets automatically once each window rolls over, and the limit cannot be lifted from within sub2api.',
allPrivacyModes: 'All Privacy States',
privacyUnset: 'Unset',
privacyTrainingOff: 'Training data sharing disabled',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index abb8dff730e..6106aa5a501 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -3143,6 +3143,7 @@ export default {
expiresAt: '过期时间',
actions: '操作'
},
+ usageWindowsHint: '“5h / 7d”是上游账号(如 OpenAI ChatGPT、Claude)官方的滚动用量窗口限制,由上游对账号设定,并非 sub2api 配置,也与你映射的模型无关。窗口滚动到期后用量会自动重置,无法在 sub2api 端解除该限制。',
allPrivacyModes: '全部Privacy状态',
privacyUnset: '未设置',
privacyTrainingOff: '已关闭训练数据共享',
diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue
index c602225c2a6..04b46a8de04 100644
--- a/frontend/src/views/admin/AccountsView.vue
+++ b/frontend/src/views/admin/AccountsView.vue
@@ -273,6 +273,12 @@
+
+
+ {{ column.label }}
+
+
+
({
+ listAccounts: vi.fn(),
+ listWithEtag: vi.fn(),
+ getBatchTodayStats: vi.fn(),
+ getAllProxies: vi.fn(),
+ getAllGroups: vi.fn()
+}))
+
+vi.mock('@/api/admin', () => ({
+ adminAPI: {
+ accounts: {
+ list: listAccounts,
+ listWithEtag,
+ getBatchTodayStats,
+ delete: vi.fn(),
+ batchClearError: vi.fn(),
+ batchRefresh: vi.fn(),
+ toggleSchedulable: vi.fn()
+ },
+ proxies: {
+ getAll: getAllProxies
+ },
+ groups: {
+ getAll: getAllGroups
+ }
+ }
+}))
+
+vi.mock('@/stores/app', () => ({
+ useAppStore: () => ({
+ showError: vi.fn(),
+ showSuccess: vi.fn(),
+ showInfo: vi.fn()
+ })
+}))
+
+vi.mock('@/stores/auth', () => ({
+ useAuthStore: () => ({
+ token: 'test-token'
+ })
+}))
+
+vi.mock('vue-i18n', async () => {
+ const actual = await vi.importActual('vue-i18n')
+ return {
+ ...actual,
+ useI18n: () => ({
+ t: (key: string) => key
+ })
+ }
+})
+
+// Render the per-column header slots so we can assert the usage-window header hint.
+const DataTableStub = {
+ props: ['columns', 'data'],
+ template: `
+
+ `
+}
+
+// Expose the content passed to HelpTooltip without dealing with its .
+const HelpTooltipStub = {
+ props: ['content', 'widthClass'],
+ template: '{{ content }}'
+}
+
+function mountView() {
+ return mount(AccountsView, {
+ global: {
+ stubs: {
+ AppLayout: { template: '
' },
+ TablePageLayout: {
+ template: '
'
+ },
+ DataTable: DataTableStub,
+ HelpTooltip: HelpTooltipStub,
+ Pagination: true,
+ ConfirmDialog: true,
+ AccountTableActions: { template: '
' },
+ AccountTableFilters: { template: '' },
+ AccountBulkActionsBar: true,
+ AccountActionMenu: true,
+ ImportDataModal: true,
+ ReAuthAccountModal: true,
+ AccountTestModal: true,
+ AccountStatsModal: true,
+ ScheduledTestsPanel: true,
+ SyncFromCrsModal: true,
+ TempUnschedStatusModal: true,
+ ErrorPassthroughRulesModal: true,
+ TLSFingerprintProfilesModal: true,
+ CreateAccountModal: true,
+ EditAccountModal: true,
+ BulkEditAccountModal: true,
+ PlatformTypeBadge: true,
+ AccountCapacityCell: true,
+ AccountStatusIndicator: true,
+ AccountTodayStatsCell: true,
+ AccountGroupsCell: true,
+ AccountUsageCell: true,
+ Icon: true
+ }
+ }
+ })
+}
+
+describe('admin AccountsView usage windows hint', () => {
+ beforeEach(() => {
+ localStorage.clear()
+
+ listAccounts.mockReset()
+ listWithEtag.mockReset()
+ getBatchTodayStats.mockReset()
+ getAllProxies.mockReset()
+ getAllGroups.mockReset()
+
+ listAccounts.mockResolvedValue({
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 20,
+ pages: 0
+ })
+ listWithEtag.mockResolvedValue({
+ notModified: true,
+ etag: null,
+ data: null
+ })
+ getBatchTodayStats.mockResolvedValue({ stats: {} })
+ getAllProxies.mockResolvedValue([])
+ getAllGroups.mockResolvedValue([])
+ })
+
+ it('renders an explanatory tooltip next to the usage windows column header', async () => {
+ const wrapper = mountView()
+ await flushPromises()
+
+ const header = wrapper.find('[data-test="usage-header"]')
+ expect(header.exists()).toBe(true)
+ // Column label is still shown alongside the help icon.
+ expect(header.text()).toContain('admin.accounts.columns.usageWindows')
+
+ const hint = wrapper.find('[data-test="usage-windows-hint"]')
+ expect(hint.exists()).toBe(true)
+ expect(hint.text()).toBe('admin.accounts.usageWindowsHint')
+ })
+})