mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
feat(tabs): implement LRU management and tab hibernation features
- Introduced `TabLRUManager` to manage tab hibernation based on usage patterns, enhancing memory efficiency. - Added `isDormant` and `savedState` properties to the `Tab` interface for tracking tab states during hibernation. - Updated `useTabs` hook to include methods for hibernating and waking tabs, along with pinning functionality to exempt tabs from LRU management. - Enhanced `AppShell` to only render non-dormant tabs, improving user experience by focusing on active content. - Implemented logging for tab state changes to facilitate debugging and monitoring of tab behavior. These changes significantly improve the application's performance and user experience by optimizing tab management and memory usage.
This commit is contained in:
parent
ec3c9db9ff
commit
4f7a14b044
15
packages/shared/data/cache/cacheValueTypes.ts
vendored
15
packages/shared/data/cache/cacheValueTypes.ts
vendored
@ -26,6 +26,14 @@ export type CacheTopic = Topic
|
||||
*/
|
||||
export type TabType = 'route' | 'webview'
|
||||
|
||||
/**
|
||||
* Tab saved state for hibernation recovery
|
||||
*/
|
||||
export interface TabSavedState {
|
||||
scrollPosition?: number
|
||||
// 其他必要草稿字段可在此扩展
|
||||
}
|
||||
|
||||
export interface Tab {
|
||||
id: string
|
||||
type: TabType
|
||||
@ -33,8 +41,11 @@ export interface Tab {
|
||||
title: string
|
||||
icon?: string
|
||||
metadata?: Record<string, unknown>
|
||||
// TODO: LRU 优化字段,后续添加
|
||||
// lastAccessTime?: number
|
||||
// LRU 字段
|
||||
lastAccessTime?: number // open/switch 时更新
|
||||
isDormant?: boolean // 是否已休眠
|
||||
isPinned?: boolean // 是否置顶(豁免 LRU)
|
||||
savedState?: TabSavedState // 休眠前保存的状态
|
||||
}
|
||||
|
||||
export interface TabsState {
|
||||
|
||||
@ -43,20 +43,21 @@ export const AppShell = () => {
|
||||
{/* Zone 1: Sidebar */}
|
||||
<Sidebar />
|
||||
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col">
|
||||
<div className="flex h-full w-full flex-1 flex-col overflow-hidden">
|
||||
{/* Zone 2: Tab Bar */}
|
||||
<Tabs value={activeTabId} onValueChange={setActiveTab} variant="line" className="w-full">
|
||||
<header className="flex h-10 w-full items-center border-b bg-muted/5">
|
||||
<TabsList className="hide-scrollbar h-full flex-1 justify-start gap-0 overflow-x-auto">
|
||||
<TabsList className="flex h-full min-w-0 flex-1 justify-start gap-0 overflow-hidden">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className={cn(
|
||||
'group relative flex h-full min-w-[120px] max-w-[200px] items-center justify-between gap-2 rounded-none border-r px-3 text-sm',
|
||||
'group relative flex h-full min-w-0 max-w-[200px] flex-1 items-center justify-between gap-2 rounded-none border-r px-3 text-sm',
|
||||
tab.id === activeTabId ? 'bg-background' : 'bg-transparent'
|
||||
)}>
|
||||
<span className="truncate text-xs">{tab.title}</span>
|
||||
{/* TODO: pin功能,形式还未确定 */}
|
||||
<span className={cn('truncate text-xs', tab.isDormant && 'opacity-60')}>{tab.title}</span>
|
||||
{tabs.length > 1 && (
|
||||
<div
|
||||
role="button"
|
||||
@ -84,9 +85,9 @@ export const AppShell = () => {
|
||||
|
||||
{/* Zone 3: Content Area - Multi MemoryRouter Architecture */}
|
||||
<main className="relative flex-1 overflow-hidden bg-background">
|
||||
{/* Route Tabs: Each has independent MemoryRouter */}
|
||||
{/* Route Tabs: Only render non-dormant tabs */}
|
||||
{tabs
|
||||
.filter((t) => t.type === 'route')
|
||||
.filter((t) => t.type === 'route' && !t.isDormant)
|
||||
.map((tab) => (
|
||||
<TabRouter
|
||||
key={tab.id}
|
||||
@ -96,9 +97,9 @@ export const AppShell = () => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Webview Tabs */}
|
||||
{/* Webview Tabs: Only render non-dormant tabs */}
|
||||
{tabs
|
||||
.filter((t) => t.type === 'webview')
|
||||
.filter((t) => t.type === 'webview' && !t.isDormant)
|
||||
.map((tab) => (
|
||||
<WebviewContainer key={tab.id} url={tab.url} isActive={tab.id === activeTabId} />
|
||||
))}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { loggerService } from '@logger'
|
||||
import { TabLRUManager } from '@renderer/services/TabLRUManager'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import { usePersistCache } from '../data/hooks/useCache'
|
||||
import { uuid } from '../utils'
|
||||
@ -6,13 +8,17 @@ import { getDefaultRouteTitle } from '../utils/routeTitle'
|
||||
|
||||
// Re-export types from shared schema
|
||||
export type { Tab, TabsState, TabType } from '@shared/data/cache/cacheValueTypes'
|
||||
import type { Tab, TabType } from '@shared/data/cache/cacheValueTypes'
|
||||
import type { Tab, TabSavedState, TabType } from '@shared/data/cache/cacheValueTypes'
|
||||
|
||||
const logger = loggerService.withContext('useTabs')
|
||||
|
||||
const DEFAULT_TAB: Tab = {
|
||||
id: 'home',
|
||||
type: 'route',
|
||||
url: '/',
|
||||
title: getDefaultRouteTitle('/')
|
||||
title: getDefaultRouteTitle('/'),
|
||||
lastAccessTime: Date.now(),
|
||||
isDormant: false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,6 +38,13 @@ export interface OpenTabOptions {
|
||||
export function useTabs() {
|
||||
const [tabsState, setTabsState] = usePersistCache('ui.tab.state')
|
||||
|
||||
// LRU 管理器(单例)
|
||||
const lruManagerRef = useRef<TabLRUManager | null>(null)
|
||||
if (!lruManagerRef.current) {
|
||||
lruManagerRef.current = new TabLRUManager()
|
||||
}
|
||||
const lruManager = lruManagerRef.current
|
||||
|
||||
// Ensure at least one default tab exists
|
||||
useEffect(() => {
|
||||
if (tabsState.tabs.length === 0) {
|
||||
@ -42,17 +55,144 @@ export function useTabs() {
|
||||
const tabs = useMemo(() => (tabsState.tabs.length > 0 ? tabsState.tabs : [DEFAULT_TAB]), [tabsState.tabs])
|
||||
const activeTabId = tabsState.activeTabId || DEFAULT_TAB.id
|
||||
|
||||
/**
|
||||
* 内部方法:执行休眠检查并休眠超额标签
|
||||
*/
|
||||
const performHibernationCheck = useCallback(
|
||||
(currentTabs: Tab[], newActiveTabId: string) => {
|
||||
const toHibernate = lruManager.checkAndGetDormantCandidates(currentTabs, newActiveTabId)
|
||||
|
||||
if (toHibernate.length === 0) {
|
||||
return currentTabs
|
||||
}
|
||||
|
||||
// 批量休眠
|
||||
return currentTabs.map((tab) => {
|
||||
if (toHibernate.includes(tab.id)) {
|
||||
logger.info('Tab hibernated', { tabId: tab.id, route: tab.url })
|
||||
// TODO: 保存滚动位置等状态
|
||||
const savedState: TabSavedState = { scrollPosition: 0 }
|
||||
return { ...tab, isDormant: true, savedState }
|
||||
}
|
||||
return tab
|
||||
})
|
||||
},
|
||||
[lruManager]
|
||||
)
|
||||
|
||||
/**
|
||||
* 休眠标签(手动)
|
||||
*
|
||||
* TODO: 目前 savedState 仅为占位符,后续需实现:
|
||||
* - 捕获真实滚动位置
|
||||
* - 保存必要的草稿/表单状态
|
||||
*/
|
||||
const hibernateTab = useCallback(
|
||||
(tabId: string) => {
|
||||
const tab = tabsState.tabs.find((t) => t.id === tabId)
|
||||
if (!tab || tab.isDormant) return
|
||||
|
||||
// TODO: 实现真实的状态捕获
|
||||
const savedState: TabSavedState = { scrollPosition: 0 }
|
||||
|
||||
logger.info('Tab hibernated (manual)', { tabId, route: tab.url })
|
||||
|
||||
setTabsState({
|
||||
...tabsState,
|
||||
tabs: tabsState.tabs.map((t) => (t.id === tabId ? { ...t, isDormant: true, savedState } : t))
|
||||
})
|
||||
},
|
||||
[tabsState, setTabsState]
|
||||
)
|
||||
|
||||
/**
|
||||
* 唤醒标签
|
||||
*
|
||||
* TODO: 目前仅清除 isDormant 标记,后续需实现:
|
||||
* - 从 savedState 恢复滚动位置
|
||||
* - 恢复草稿/表单状态
|
||||
*/
|
||||
const wakeTab = useCallback(
|
||||
(tabId: string) => {
|
||||
const tab = tabsState.tabs.find((t) => t.id === tabId)
|
||||
if (!tab || !tab.isDormant) return
|
||||
|
||||
logger.info('Tab awakened', { tabId, route: tab.url })
|
||||
|
||||
// TODO: 实现真实的状态恢复(从 tab.savedState)
|
||||
setTabsState({
|
||||
...tabsState,
|
||||
tabs: tabsState.tabs.map((t) => (t.id === tabId ? { ...t, isDormant: false, lastAccessTime: Date.now() } : t))
|
||||
})
|
||||
},
|
||||
[tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const updateTab = useCallback(
|
||||
(id: string, updates: Partial<Tab>) => {
|
||||
setTabsState({
|
||||
...tabsState,
|
||||
tabs: tabsState.tabs.map((t) => (t.id === id ? { ...t, ...updates } : t))
|
||||
})
|
||||
},
|
||||
[tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(id: string) => {
|
||||
if (id === activeTabId) return
|
||||
|
||||
const targetTab = tabs.find((t) => t.id === id)
|
||||
if (!targetTab) return
|
||||
|
||||
// 1. 准备更新后的标签列表
|
||||
let updatedTabs = tabsState.tabs.map((t) =>
|
||||
t.id === id
|
||||
? {
|
||||
...t,
|
||||
lastAccessTime: Date.now(),
|
||||
// 如果目标是休眠状态,唤醒它
|
||||
isDormant: false
|
||||
}
|
||||
: t
|
||||
)
|
||||
|
||||
// 2. 如果唤醒了休眠标签,记录日志
|
||||
if (targetTab.isDormant) {
|
||||
logger.info('Tab awakened', { tabId: id, route: targetTab.url })
|
||||
}
|
||||
|
||||
// 3. 执行休眠检查(可能需要休眠其他标签)
|
||||
updatedTabs = performHibernationCheck(updatedTabs, id)
|
||||
|
||||
// 4. 更新状态
|
||||
setTabsState({ tabs: updatedTabs, activeTabId: id })
|
||||
},
|
||||
[activeTabId, tabs, tabsState, setTabsState, performHibernationCheck]
|
||||
)
|
||||
|
||||
const addTab = useCallback(
|
||||
(tab: Tab) => {
|
||||
const exists = tabs.find((t) => t.id === tab.id)
|
||||
if (exists) {
|
||||
setTabsState({ ...tabsState, activeTabId: tab.id })
|
||||
setActiveTab(tab.id)
|
||||
return
|
||||
}
|
||||
const newTabs = [...tabs, tab]
|
||||
|
||||
// 添加 LRU 字段
|
||||
const newTab: Tab = {
|
||||
...tab,
|
||||
lastAccessTime: Date.now(),
|
||||
isDormant: false
|
||||
}
|
||||
|
||||
// 执行休眠检查
|
||||
let newTabs = [...tabs, newTab]
|
||||
newTabs = performHibernationCheck(newTabs, tab.id)
|
||||
|
||||
setTabsState({ tabs: newTabs, activeTabId: tab.id })
|
||||
},
|
||||
[tabs, tabsState, setTabsState]
|
||||
[tabs, setTabsState, setActiveTab, performHibernationCheck]
|
||||
)
|
||||
|
||||
const closeTab = useCallback(
|
||||
@ -71,23 +211,6 @@ export function useTabs() {
|
||||
[tabs, activeTabId, setTabsState]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(id: string) => {
|
||||
if (id !== activeTabId) {
|
||||
setTabsState({ ...tabsState, activeTabId: id })
|
||||
}
|
||||
},
|
||||
[activeTabId, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const updateTab = useCallback(
|
||||
(id: string, updates: Partial<Tab>) => {
|
||||
const newTabs = tabs.map((t) => (t.id === id ? { ...t, ...updates } : t))
|
||||
setTabsState({ ...tabsState, tabs: newTabs })
|
||||
},
|
||||
[tabs, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const setTabs = useCallback(
|
||||
(newTabs: Tab[] | ((prev: Tab[]) => Tab[])) => {
|
||||
const resolvedTabs = typeof newTabs === 'function' ? newTabs(tabs) : newTabs
|
||||
@ -128,12 +251,14 @@ export function useTabs() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create new tab with default route title
|
||||
// Create new tab with default route title and LRU fields
|
||||
const newTab: Tab = {
|
||||
id: id || uuid(),
|
||||
type,
|
||||
url,
|
||||
title: title || getDefaultRouteTitle(url)
|
||||
title: title || getDefaultRouteTitle(url),
|
||||
lastAccessTime: Date.now(),
|
||||
isDormant: false
|
||||
}
|
||||
|
||||
addTab(newTab)
|
||||
@ -142,6 +267,28 @@ export function useTabs() {
|
||||
[tabs, setActiveTab, addTab]
|
||||
)
|
||||
|
||||
/**
|
||||
* Pin a tab (exempt from LRU hibernation)
|
||||
*/
|
||||
const pinTab = useCallback(
|
||||
(id: string) => {
|
||||
updateTab(id, { isPinned: true })
|
||||
logger.info('Tab pinned', { tabId: id })
|
||||
},
|
||||
[updateTab]
|
||||
)
|
||||
|
||||
/**
|
||||
* Unpin a tab
|
||||
*/
|
||||
const unpinTab = useCallback(
|
||||
(id: string) => {
|
||||
updateTab(id, { isPinned: false })
|
||||
logger.info('Tab unpinned', { tabId: id })
|
||||
},
|
||||
[updateTab]
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the currently active tab
|
||||
*/
|
||||
@ -162,6 +309,12 @@ export function useTabs() {
|
||||
setTabs,
|
||||
|
||||
// High-level Tab operations
|
||||
openTab
|
||||
openTab,
|
||||
|
||||
// LRU operations
|
||||
hibernateTab,
|
||||
wakeTab,
|
||||
pinTab,
|
||||
unpinTab
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,18 +34,14 @@ const HomePage: FC = () => {
|
||||
const search = useSearch({ strict: false }) as { assistantId?: string; topicId?: string }
|
||||
|
||||
// 根据 search params 中的 ID 查找对应的 assistant
|
||||
const assistantFromSearch = search.assistantId
|
||||
? assistants.find((a) => a.id === search.assistantId)
|
||||
: undefined
|
||||
const assistantFromSearch = search.assistantId ? assistants.find((a) => a.id === search.assistantId) : undefined
|
||||
|
||||
const [activeAssistant, _setActiveAssistant] = useState<Assistant>(
|
||||
assistantFromSearch || _activeAssistant || assistants[0]
|
||||
)
|
||||
|
||||
// 根据 search params 中的 topicId 查找对应的 topic
|
||||
const topicFromSearch = search.topicId
|
||||
? activeAssistant?.topics?.find((t) => t.id === search.topicId)
|
||||
: undefined
|
||||
const topicFromSearch = search.topicId ? activeAssistant?.topics?.find((t) => t.id === search.topicId) : undefined
|
||||
|
||||
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', topicFromSearch)
|
||||
const [showAssistants] = usePreference('assistant.tab.show')
|
||||
|
||||
@ -68,11 +68,7 @@ const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
|
||||
values={{ provider: getProviderLabel(providerId) }}
|
||||
components={{
|
||||
provider: (
|
||||
<Link
|
||||
style={{ color: 'var(--color-link)' }}
|
||||
to="/settings/provider"
|
||||
search={{ id: providerId }}
|
||||
/>
|
||||
<Link style={{ color: 'var(--color-link)' }} to="/settings/provider" search={{ id: providerId }} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
198
src/renderer/src/services/TabLRUManager.ts
Normal file
198
src/renderer/src/services/TabLRUManager.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { Tab } from '@shared/data/cache/cacheValueTypes'
|
||||
|
||||
const logger = loggerService.withContext('TabLRU')
|
||||
|
||||
/**
|
||||
* Tab LRU limits configuration
|
||||
*
|
||||
* Controls when inactive tabs should be hibernated to save memory.
|
||||
* TODO: 后续可从偏好设置注入
|
||||
*/
|
||||
export const TAB_LIMITS = {
|
||||
/**
|
||||
* 软上限:活跃标签数超过此值时触发 LRU 休眠
|
||||
* 默认 10,可根据实际内存使用情况调整
|
||||
*/
|
||||
softCap: 10,
|
||||
|
||||
/**
|
||||
* 硬保险丝:极端兜底,防止 runaway
|
||||
* 当活跃标签数超过此值时,强制休眠超额部分
|
||||
*/
|
||||
hardCap: 22
|
||||
}
|
||||
|
||||
export type TabLimits = typeof TAB_LIMITS
|
||||
|
||||
/**
|
||||
* TabLRUManager - 管理标签页的 LRU 休眠策略
|
||||
*
|
||||
* 功能:
|
||||
* - 当活跃标签数超过软上限时,选择 LRU 候选进行休眠
|
||||
* - 硬保险丝作为极端兜底,防止内存失控
|
||||
* - 支持豁免机制:当前标签、首页、置顶标签不参与休眠
|
||||
*/
|
||||
export class TabLRUManager {
|
||||
private softCap: number
|
||||
private hardCap: number
|
||||
|
||||
constructor(limits: TabLimits = TAB_LIMITS) {
|
||||
this.softCap = limits.softCap
|
||||
this.hardCap = limits.hardCap
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并返回需要休眠的标签 ID 列表
|
||||
*
|
||||
* 策略:
|
||||
* - 超过 softCap:休眠到 softCap
|
||||
* - 超过 hardCap:强制休眠到 softCap(忽略部分豁免,仅保留当前+首页)
|
||||
*
|
||||
* @param tabs 所有标签
|
||||
* @param activeTabId 当前活动标签 ID
|
||||
* @returns 需要休眠的标签 ID 数组
|
||||
*/
|
||||
checkAndGetDormantCandidates(tabs: Tab[], activeTabId: string): string[] {
|
||||
const activeTabs = tabs.filter((t) => !t.isDormant)
|
||||
const activeCount = activeTabs.length
|
||||
|
||||
// 未超软上限,无需休眠
|
||||
if (activeCount <= this.softCap) {
|
||||
return []
|
||||
}
|
||||
|
||||
const isHardCapTriggered = activeCount > this.hardCap
|
||||
|
||||
// 获取候选列表
|
||||
// 硬保险丝触发时,使用更宽松的豁免规则(仅保留当前+首页)
|
||||
const candidates = isHardCapTriggered
|
||||
? this.getHardCapCandidates(activeTabs, activeTabId)
|
||||
: this.getLRUCandidates(activeTabs, activeTabId)
|
||||
|
||||
// 计算需要休眠的数量:始终休眠到 softCap
|
||||
let toHibernateCount = activeCount - this.softCap
|
||||
|
||||
if (isHardCapTriggered) {
|
||||
logger.warn('Hard cap triggered - using relaxed exemption rules', {
|
||||
activeCount,
|
||||
hardCap: this.hardCap,
|
||||
softCap: this.softCap,
|
||||
toHibernate: toHibernateCount
|
||||
})
|
||||
}
|
||||
|
||||
// 只能休眠可用的候选数量
|
||||
toHibernateCount = Math.min(toHibernateCount, candidates.length)
|
||||
|
||||
// 检查是否能达到目标
|
||||
const afterHibernation = activeCount - toHibernateCount
|
||||
if (isHardCapTriggered && afterHibernation > this.hardCap) {
|
||||
// 极端情况:即使放宽豁免,仍无法降到 hardCap 以下
|
||||
logger.error('Cannot guarantee hard cap - insufficient candidates', {
|
||||
activeCount,
|
||||
candidatesAvailable: candidates.length,
|
||||
willHibernate: toHibernateCount,
|
||||
afterHibernation,
|
||||
hardCap: this.hardCap
|
||||
})
|
||||
} else if (afterHibernation > this.softCap) {
|
||||
// 一般情况:无法降到 softCap,但仍在 hardCap 以下
|
||||
logger.warn('Cannot reach soft cap - limited by available candidates', {
|
||||
activeCount,
|
||||
candidatesAvailable: candidates.length,
|
||||
willHibernate: toHibernateCount,
|
||||
afterHibernation,
|
||||
softCap: this.softCap
|
||||
})
|
||||
}
|
||||
|
||||
const result = candidates.slice(0, toHibernateCount).map((t) => t.id)
|
||||
|
||||
if (result.length > 0) {
|
||||
logger.info('Tabs selected for hibernation', {
|
||||
count: result.length,
|
||||
ids: result,
|
||||
activeCount,
|
||||
softCap: this.softCap,
|
||||
hardCapTriggered: isHardCapTriggered
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬保险丝候选列表(仅豁免当前标签和首页)
|
||||
*/
|
||||
private getHardCapCandidates(tabs: Tab[], activeTabId: string): Tab[] {
|
||||
return tabs
|
||||
.filter((tab) => !this.isHardExempt(tab, activeTabId))
|
||||
.sort((a, b) => (a.lastAccessTime ?? 0) - (b.lastAccessTime ?? 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬保险丝豁免判断(更严格,仅保留当前+首页)
|
||||
*/
|
||||
private isHardExempt(tab: Tab, activeTabId: string): boolean {
|
||||
return (
|
||||
tab.id === activeTabId || // 当前活动标签
|
||||
tab.id === 'home' || // 首页
|
||||
tab.isDormant === true // 已休眠的不再参与
|
||||
)
|
||||
// 注意:isPinned 在硬保险丝触发时不再豁免
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 LRU 候选列表(排除豁免项,按访问时间升序)
|
||||
*/
|
||||
private getLRUCandidates(tabs: Tab[], activeTabId: string): Tab[] {
|
||||
return tabs
|
||||
.filter((tab) => !this.isExempt(tab, activeTabId))
|
||||
.sort((a, b) => (a.lastAccessTime ?? 0) - (b.lastAccessTime ?? 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断标签是否豁免休眠
|
||||
*
|
||||
* 豁免条件:
|
||||
* - 当前活动标签
|
||||
* - 首页标签 (id === 'home')
|
||||
* - 置顶标签 (isPinned)
|
||||
* - 已休眠的标签(不重复处理)
|
||||
*/
|
||||
private isExempt(tab: Tab, activeTabId: string): boolean {
|
||||
return (
|
||||
tab.id === activeTabId || // 当前活动标签
|
||||
tab.id === 'home' || // 首页
|
||||
tab.isPinned === true || // 置顶标签
|
||||
tab.isDormant === true // 已休眠的不再参与
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新软上限(供未来设置页使用)
|
||||
*/
|
||||
updateSoftCap(newSoftCap: number): void {
|
||||
this.softCap = newSoftCap
|
||||
logger.info('SoftCap updated', { newSoftCap })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新硬上限(供未来设置页使用)
|
||||
*/
|
||||
updateHardCap(newHardCap: number): void {
|
||||
this.hardCap = newHardCap
|
||||
logger.info('HardCap updated', { newHardCap })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
getLimits(): TabLimits {
|
||||
return {
|
||||
softCap: this.softCap,
|
||||
hardCap: this.hardCap
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user