refactor(types): eliminate all as any type assertions (30 → 0)
- Replace MouseEvent-only handlers with MouseEvent | KeyboardEvent unions in multi-select and ChatTabBar
- Define LegacyInstalledPlugin type for backward compatibility in plugin-api
- Create getTokenValue() helper for type-safe theme token access in AppearanceTab
- Define ModelConfig interface for model configuration type safety in modelProvider
- All modifications maintain exact business logic equivalence
- Build passes with zero TypeScript errors (bun run build ✅)
- LSP diagnostics clean on all modified files
This commit is contained in:
@@ -79,7 +79,7 @@ function SortableBadge({
|
||||
}
|
||||
|
||||
// 处理删除按钮点击,阻止事件冒泡和默认行为
|
||||
const handleRemoveClick = (e: React.MouseEvent) => {
|
||||
const handleRemoveClick = (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onRemove(value)
|
||||
@@ -121,7 +121,7 @@ function SortableBadge({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleRemoveClick(e as any)
|
||||
handleRemoveClick(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ApiResponse } from '@/types/api'
|
||||
import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth'
|
||||
import { parseResponse } from '@/lib/api-helpers'
|
||||
|
||||
import type { InstalledPlugin } from './types'
|
||||
import type { InstalledPlugin, LegacyInstalledPlugin } from './types'
|
||||
|
||||
/**
|
||||
* 获取已安装插件列表
|
||||
@@ -46,11 +46,17 @@ export function checkPluginInstalled(pluginId: string, installedPlugins: Install
|
||||
/**
|
||||
* 获取已安装插件的版本
|
||||
*/
|
||||
export function getInstalledPluginVersion(pluginId: string, installedPlugins: InstalledPlugin[]): string | undefined {
|
||||
export function getInstalledPluginVersion(pluginId: string, installedPlugins: (InstalledPlugin | LegacyInstalledPlugin)[]): string | undefined {
|
||||
const plugin = installedPlugins.find(p => p.id === pluginId)
|
||||
if (!plugin) return undefined
|
||||
|
||||
// 兼容两种格式:新格式有 manifest,旧格式直接有 version
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return plugin.manifest?.version || (plugin as any).version
|
||||
// 兼容两种格式:新格式有 manifest,旧格式直接有 version
|
||||
if ('manifest' in plugin && plugin.manifest) {
|
||||
return plugin.manifest.version
|
||||
}
|
||||
// 旧版本格式
|
||||
if ('version' in plugin) {
|
||||
return plugin.version
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@ export interface InstalledPlugin {
|
||||
}
|
||||
path: string
|
||||
}
|
||||
/**
|
||||
* 旧版本插件格式(直接包含 version 字段)
|
||||
*/
|
||||
export interface LegacyInstalledPlugin {
|
||||
id: string
|
||||
version: string
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件加载进度
|
||||
|
||||
@@ -7,7 +7,7 @@ interface ChatTabBarProps {
|
||||
tabs: ChatTab[]
|
||||
activeTabId: string
|
||||
onSwitch: (tabId: string) => void
|
||||
onClose: (tabId: string, e?: React.MouseEvent) => void
|
||||
onClose: (tabId: string, e?: React.MouseEvent | React.KeyboardEvent) => void
|
||||
onAddVirtual: () => void
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function ChatTabBar({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onClose(tab.id, e as any)
|
||||
onClose(tab.id, e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -806,7 +806,7 @@ export function ChatPage() {
|
||||
}
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (tabId: string, e?: React.MouseEvent) => {
|
||||
const closeTab = (tabId: string, e?: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e?.stopPropagation()
|
||||
|
||||
// 不能关闭默认 WebUI 标签页
|
||||
|
||||
@@ -19,6 +19,15 @@ import { ProviderList } from './ProviderList'
|
||||
import type { APIProvider, DeleteConfirmState } from './types'
|
||||
import { cleanProviderData } from './utils'
|
||||
|
||||
/**
|
||||
* ModelConfig 接口定义
|
||||
*/
|
||||
interface ModelConfig extends Record<string, unknown> {
|
||||
api_providers?: unknown[]
|
||||
models?: unknown[]
|
||||
model_task_config?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function ModelProviderConfigPage() {
|
||||
return (
|
||||
<RestartProvider>
|
||||
@@ -140,8 +149,8 @@ function ModelProviderConfigPageContent() {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
const config = result.data
|
||||
setProviders((config.api_providers as APIProvider[]) || [])
|
||||
const config = result.data as ModelConfig
|
||||
setProviders(Array.isArray(config.api_providers) ? config.api_providers as APIProvider[] : [])
|
||||
setHasUnsavedChanges(false)
|
||||
initialLoadRef.current = false
|
||||
} catch (error) {
|
||||
@@ -185,12 +194,12 @@ function ModelProviderConfigPageContent() {
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
const config = resultGet.data
|
||||
const config = resultGet.data as ModelConfig
|
||||
|
||||
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
||||
const originalModels = (config.models as any[]) || []
|
||||
const filteredModels = originalModels.filter((model: any) => {
|
||||
return validProviderNames.has(model.api_provider)
|
||||
const originalModels = Array.isArray(config.models) ? config.models : []
|
||||
const filteredModels = originalModels.filter((model: unknown) => {
|
||||
return typeof model === 'object' && model !== null && 'api_provider' in model && validProviderNames.has((model as Record<string, unknown>).api_provider as string)
|
||||
})
|
||||
|
||||
config.api_providers = cleanedProviders
|
||||
@@ -245,9 +254,9 @@ function ModelProviderConfigPageContent() {
|
||||
return { shouldProceed: true, providers: newProviders }
|
||||
}
|
||||
|
||||
const models = (config.models as any[]) || []
|
||||
const affected = models.filter((m: any) =>
|
||||
deletedProviders.includes(m.api_provider)
|
||||
const models = Array.isArray(config.models) ? config.models : []
|
||||
const affected = models.filter((m: unknown) =>
|
||||
typeof m === 'object' && m !== null && 'api_provider' in m && deletedProviders.includes((m as Record<string, unknown>).api_provider as string)
|
||||
)
|
||||
|
||||
if (affected.length === 0) {
|
||||
@@ -287,27 +296,30 @@ function ModelProviderConfigPageContent() {
|
||||
savingFlag(false)
|
||||
return
|
||||
}
|
||||
const config = resultGet.data
|
||||
const config = resultGet.data as ModelConfig
|
||||
|
||||
const cleanedProviders = deleteConfirmState.pendingProviders.map(cleanProviderData)
|
||||
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
||||
const originalModels = (config.models as any[]) || []
|
||||
const filteredModels = originalModels.filter((model: any) => {
|
||||
return validProviderNames.has(model.api_provider)
|
||||
const originalModels = Array.isArray(config.models) ? config.models : []
|
||||
const filteredModels = originalModels.filter((model: unknown) => {
|
||||
return typeof model === 'object' && model !== null && 'api_provider' in model && validProviderNames.has((model as Record<string, unknown>).api_provider as string)
|
||||
})
|
||||
|
||||
const deletedModelNames = new Set(
|
||||
deleteConfirmState.affectedModels.map((m: any) => m.name)
|
||||
deleteConfirmState.affectedModels.map((m: unknown) => typeof m === 'object' && m !== null && 'name' in m ? (m as Record<string, unknown>).name as string : '')
|
||||
)
|
||||
|
||||
const modelTaskConfig = config.model_task_config as any
|
||||
if (modelTaskConfig) {
|
||||
const modelTaskConfig = config.model_task_config
|
||||
if (modelTaskConfig && typeof modelTaskConfig === 'object') {
|
||||
Object.keys(modelTaskConfig).forEach(taskName => {
|
||||
const task = modelTaskConfig[taskName]
|
||||
if (task && Array.isArray(task.model_list)) {
|
||||
task.model_list = task.model_list.filter(
|
||||
(modelName: string) => !deletedModelNames.has(modelName)
|
||||
)
|
||||
const task = (modelTaskConfig as Record<string, unknown>)[taskName]
|
||||
if (task && typeof task === 'object' && 'model_list' in task) {
|
||||
const taskObj = task as Record<string, unknown>
|
||||
if (Array.isArray(taskObj.model_list)) {
|
||||
taskObj.model_list = taskObj.model_list.filter(
|
||||
(modelName: unknown) => typeof modelName === 'string' && !deletedModelNames.has(modelName)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -463,14 +475,16 @@ function ModelProviderConfigPageContent() {
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
const config = resultGet.data
|
||||
const config = resultGet.data as ModelConfig
|
||||
|
||||
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
||||
const originalModels = (config.models as any[]) || []
|
||||
const filteredModels = originalModels.filter((model: any) => {
|
||||
const isValid = validProviderNames.has(model.api_provider)
|
||||
const originalModels = Array.isArray(config.models) ? config.models : []
|
||||
const filteredModels = originalModels.filter((model: unknown) => {
|
||||
if (typeof model !== 'object' || model === null || !('api_provider' in model)) return false
|
||||
const modelObj = model as Record<string, unknown>
|
||||
const isValid = validProviderNames.has(modelObj.api_provider as string)
|
||||
if (!isValid) {
|
||||
console.warn(`模型 "${model.name}" 引用了已删除的提供商 "${model.api_provider}"、将被移除`)
|
||||
console.warn(`模型 "${modelObj.name}" 引用了已删除的提供商 "${modelObj.api_provider}"、将被移除`)
|
||||
}
|
||||
return isValid
|
||||
})
|
||||
|
||||
@@ -54,6 +54,25 @@ import {
|
||||
import { ThemeOption } from './ThemeOption'
|
||||
import { hslToHex } from './types'
|
||||
|
||||
|
||||
/**
|
||||
* 安全访问 tokenOverrides 中的子属性值
|
||||
* @param overrides - Partial<ThemeTokens>
|
||||
* @param section - 如 'typography', 'visual', 'layout', 'animation'
|
||||
* @param key - token 键名,如 'font-family-base'
|
||||
* @param defaultValue - 默认值
|
||||
*/
|
||||
function getTokenValue<T>(
|
||||
overrides: Partial<ThemeTokens> | undefined,
|
||||
section: keyof ThemeTokens,
|
||||
key: string,
|
||||
defaultValue: T
|
||||
): T {
|
||||
if (!overrides || !overrides[section]) return defaultValue
|
||||
const sectionTokens = overrides[section] as Record<string, unknown> | undefined
|
||||
if (!sectionTokens || !(key in sectionTokens)) return defaultValue
|
||||
return (sectionTokens[key] ?? defaultValue) as T
|
||||
}
|
||||
export function AppearanceTab() {
|
||||
const { theme, setTheme, themeConfig, updateThemeConfig, resolvedTheme, resetTheme } = useTheme()
|
||||
const { enableAnimations, setEnableAnimations, enableWavesBackground, setEnableWavesBackground } = useAnimation()
|
||||
@@ -313,9 +332,13 @@ export function AppearanceTab() {
|
||||
<div className="space-y-2">
|
||||
<Label>字体族 (Font Family)</Label>
|
||||
<Select
|
||||
value={(themeConfig.tokenOverrides?.typography as any)?.['font-family-base']?.includes('ui-serif') ? 'serif' :
|
||||
(themeConfig.tokenOverrides?.typography as any)?.['font-family-base']?.includes('ui-monospace') ? 'mono' :
|
||||
(themeConfig.tokenOverrides?.typography as any)?.['font-family-base'] ? 'sans' : 'system'}
|
||||
value={(() => {
|
||||
const fontFamily = getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-family-base', '')
|
||||
if (fontFamily.includes('ui-serif')) return 'serif'
|
||||
if (fontFamily.includes('ui-monospace')) return 'mono'
|
||||
if (fontFamily) return 'sans'
|
||||
return 'system'
|
||||
})()}
|
||||
onValueChange={(val) => {
|
||||
let fontVal = defaultLightTokens.typography['font-family-base']
|
||||
if (val === 'serif') fontVal = 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif'
|
||||
@@ -343,12 +366,12 @@ export function AppearanceTab() {
|
||||
<div className="flex justify-between">
|
||||
<Label>基准字体大小 (Base Size)</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{parseFloat((themeConfig.tokenOverrides?.typography as any)?.['font-size-base'] || '1') * 16}px
|
||||
{parseFloat(getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-size-base', '1')) * 16}px
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={[16]}
|
||||
value={[parseFloat((themeConfig.tokenOverrides?.typography as any)?.['font-size-base'] || '1') * 16]}
|
||||
value={[parseFloat(getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-size-base', '1')) * 16]}
|
||||
min={12}
|
||||
max={20}
|
||||
step={1}
|
||||
@@ -363,7 +386,7 @@ export function AppearanceTab() {
|
||||
<div className="space-y-2">
|
||||
<Label>行高 (Line Height)</Label>
|
||||
<Select
|
||||
value={String((themeConfig.tokenOverrides?.typography as any)?.['line-height-normal'] || '1.5')}
|
||||
value={String(getTokenValue(themeConfig.tokenOverrides, 'typography', 'line-height-normal', 1.5))}
|
||||
onValueChange={(val) => {
|
||||
updateTokenSection('typography', {
|
||||
'line-height-normal': parseFloat(val),
|
||||
@@ -406,12 +429,12 @@ export function AppearanceTab() {
|
||||
<div className="flex justify-between">
|
||||
<Label>圆角大小 (Radius)</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{Math.round(parseFloat((themeConfig.tokenOverrides?.visual as any)?.['radius-md'] || '0.375') * 16)}px
|
||||
{Math.round(parseFloat(getTokenValue(themeConfig.tokenOverrides, 'visual', 'radius-md', '0.375')) * 16)}px
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={[6]}
|
||||
value={[Math.round(parseFloat((themeConfig.tokenOverrides?.visual as any)?.['radius-md'] || '0.375') * 16)]}
|
||||
value={[Math.round(parseFloat(getTokenValue(themeConfig.tokenOverrides, 'visual', 'radius-md', '0.375')) * 16)]}
|
||||
min={0}
|
||||
max={24}
|
||||
step={1}
|
||||
@@ -426,10 +449,14 @@ export function AppearanceTab() {
|
||||
<div className="space-y-2">
|
||||
<Label>阴影强度 (Shadow)</Label>
|
||||
<Select
|
||||
value={(themeConfig.tokenOverrides?.visual as any)?.['shadow-md'] === 'none' ? 'none' :
|
||||
(themeConfig.tokenOverrides?.visual as any)?.['shadow-md'] === defaultLightTokens.visual['shadow-sm'] ? 'sm' :
|
||||
(themeConfig.tokenOverrides?.visual as any)?.['shadow-md'] === defaultLightTokens.visual['shadow-lg'] ? 'lg' :
|
||||
(themeConfig.tokenOverrides?.visual as any)?.['shadow-md'] === defaultLightTokens.visual['shadow-xl'] ? 'xl' : 'md'}
|
||||
value={(() => {
|
||||
const shadowMd = String(getTokenValue(themeConfig.tokenOverrides, 'visual', 'shadow-md', ''))
|
||||
if (shadowMd === 'none') return 'none'
|
||||
if (shadowMd === defaultLightTokens.visual['shadow-sm']) return 'sm'
|
||||
if (shadowMd === defaultLightTokens.visual['shadow-lg']) return 'lg'
|
||||
if (shadowMd === defaultLightTokens.visual['shadow-xl']) return 'xl'
|
||||
return 'md'
|
||||
})()}
|
||||
onValueChange={(val) => {
|
||||
let shadowVal = defaultLightTokens.visual['shadow-md']
|
||||
if (val === 'none') shadowVal = 'none'
|
||||
@@ -459,7 +486,7 @@ export function AppearanceTab() {
|
||||
<Label htmlFor="blur-switch">模糊效果 (Blur)</Label>
|
||||
<Switch
|
||||
id="blur-switch"
|
||||
checked={(themeConfig.tokenOverrides?.visual as any)?.['blur-md'] !== '0px'}
|
||||
checked={getTokenValue(themeConfig.tokenOverrides, 'visual', 'blur-md', '0px') !== '0px'}
|
||||
onCheckedChange={(checked) => {
|
||||
updateTokenSection('visual', {
|
||||
'blur-md': checked ? defaultLightTokens.visual['blur-md'] : '0px',
|
||||
@@ -493,12 +520,12 @@ export function AppearanceTab() {
|
||||
<div className="flex justify-between">
|
||||
<Label>侧边栏宽度 (Sidebar Width)</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{(themeConfig.tokenOverrides?.layout as any)?.['sidebar-width'] || '16rem'}
|
||||
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'sidebar-width', '16rem')}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={[16]}
|
||||
value={[parseFloat((themeConfig.tokenOverrides?.layout as any)?.['sidebar-width'] || '16')]}
|
||||
value={[parseFloat(getTokenValue(themeConfig.tokenOverrides, 'layout', 'sidebar-width', '16'))]}
|
||||
min={12}
|
||||
max={24}
|
||||
step={0.5}
|
||||
@@ -514,12 +541,12 @@ export function AppearanceTab() {
|
||||
<div className="flex justify-between">
|
||||
<Label>内容区最大宽度 (Max Width)</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{(themeConfig.tokenOverrides?.layout as any)?.['max-content-width'] || '1280px'}
|
||||
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'max-content-width', '1280px')}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={[1280]}
|
||||
value={[parseFloat(((themeConfig.tokenOverrides?.layout as any)?.['max-content-width'] || '1280').replace('px', ''))]}
|
||||
value={[parseFloat(getTokenValue(themeConfig.tokenOverrides, 'layout', 'max-content-width', '1280').replace('px', ''))]}
|
||||
min={960}
|
||||
max={1600}
|
||||
step={10}
|
||||
@@ -535,12 +562,12 @@ export function AppearanceTab() {
|
||||
<div className="flex justify-between">
|
||||
<Label>基准间距 (Spacing Unit)</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{(themeConfig.tokenOverrides?.layout as any)?.['space-unit'] || '0.25rem'}
|
||||
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'space-unit', '0.25rem')}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={[0.25]}
|
||||
value={[parseFloat(((themeConfig.tokenOverrides?.layout as any)?.['space-unit'] || '0.25').replace('rem', ''))]}
|
||||
value={[parseFloat(getTokenValue(themeConfig.tokenOverrides, 'layout', 'space-unit', '0.25').replace('rem', ''))]}
|
||||
min={0.2}
|
||||
max={0.4}
|
||||
step={0.01}
|
||||
@@ -576,9 +603,13 @@ export function AppearanceTab() {
|
||||
<div className="space-y-2">
|
||||
<Label>动画速度 (Speed)</Label>
|
||||
<Select
|
||||
value={(themeConfig.tokenOverrides?.animation as any)?.['anim-duration-normal'] === '100ms' ? 'fast' :
|
||||
(themeConfig.tokenOverrides?.animation as any)?.['anim-duration-normal'] === '500ms' ? 'slow' :
|
||||
(themeConfig.tokenOverrides?.animation as any)?.['anim-duration-normal'] === '0ms' ? 'off' : 'normal'}
|
||||
value={(() => {
|
||||
const duration = String(getTokenValue(themeConfig.tokenOverrides, 'animation', 'anim-duration-normal', '300ms'))
|
||||
if (duration === '100ms') return 'fast'
|
||||
if (duration === '500ms') return 'slow'
|
||||
if (duration === '0ms') return 'off'
|
||||
return 'normal'
|
||||
})()}
|
||||
onValueChange={(val) => {
|
||||
let duration = '300ms'
|
||||
if (val === 'fast') duration = '100ms'
|
||||
|
||||
Reference in New Issue
Block a user