import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { motion, AnimatePresence } from 'framer-motion' import { AlertCircle, AlertTriangle, CandlestickChart as CandlestickIcon, Check, CheckCircle2, Database, ExternalLink, Eye, EyeOff, FileWarning, KeyRound, Landmark, ListChecks, Loader2, Lock, Plus, Puzzle, Radio, RefreshCw, Route, Save, Scale, Timer, Trash2, Zap, } from 'lucide-react' import { api, type CapabilityMatrix, type CapabilityRoute, type DataSourceItem, type PluginDataSourceItem, type Preferences, type ProviderField, } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { useCapabilities, usePreferences } from '@/lib/useSharedQueries' import { AnchorWrap } from '@/lib/useCardFlash' import { CAP_LABELS, TIER_RANK, tierRank, tierStyle, TierTag } from '@/lib/capability-labels' import { toast } from '@/components/Toast' import { DataSourceEditor } from './DataSourceEditor' import { TickFlowKeySection, TierHelpPopover, useInvalidateTierRelated } from './Keys' const DATASET_LABEL: Record = { realtime: '实时', daily: '日K', minute: '分钟', adj_factor: '除权', financial: '财务', } /** 能力图标 (纯展示; 能力清单本身由后端注册表驱动) */ const CAP_ICON: Record> = { realtime: Radio, daily: CandlestickIcon, minute: Timer, adj_factor: Scale, financial: Landmark, } /** TickFlow 档位要求文本: none → 全档位, 其余 → starter+ 形式 */ function tierReqText(tier: string): string { return tier === 'none' ? '全档位' : `${tier}+` } /** TickFlow 档位要求徽标: 按所需档位配色, 当前档位不足时琥珀描边提示。 * 仅用于 TickFlow 详情的档位介绍表 (能力卡上不展示档位信息)。 */ function TierReqChip({ tier, currentLabel }: { tier: string; currentLabel?: string }) { const text = tierReqText(tier) const req = TIER_RANK[tier] ?? -1 const unmet = currentLabel != null && tierRank(currentLabel) < req const t = tierStyle(tier) return ( {text} ) } /** TickFlow「已适配全档位」标识: Expert 三色渐变(全档位体系里最醒目的身份色) */ function AllTiersBadge({ size = 'text-[10px]' }: { size?: string }) { const t = tierStyle('expert') return ( ✦ 已适配全档位 ) } /** 提供方标签样式: 当前项高亮(accent), 其余弱化可点; 未就绪源禁用置灰 */ function tagCls(active: boolean, disabled = false) { const base = 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] transition-colors select-none' if (disabled) return `${base} bg-elevated/40 text-muted/50 cursor-not-allowed` return `${base} cursor-pointer disabled:opacity-50 ${ active ? 'bg-accent/15 text-accent font-medium' : 'bg-elevated/60 text-muted/70 hover:bg-accent/15 hover:text-accent' }` } /** 源名 → 展示名 (特殊值 + 候选/待就绪表查找, 找不到回退原始名) */ function displayOfName(matrix: CapabilityMatrix | undefined, name: string): string { if (name === 'tickflow') return 'TickFlow' if (name === 'same_as_daily') return '跟随日K' for (const cap of matrix?.capabilities ?? []) { const hit = cap.candidates.find(c => c.name === name) ?? cap.pending.find(c => c.name === name) if (hit) return hit.display } return name } /** 乐观更新: 把一组偏好字段变更应用到矩阵缓存 (每能力独立路由, current 即生效) */ function patchMatrix( matrix: CapabilityMatrix, changes: Partial>, ): CapabilityMatrix { const caps = matrix.capabilities.map(c => ({ ...c })) for (const cap of caps) { const value = changes[cap.field] if (value !== undefined) { cap.current = value cap.current_display = displayOfName({ capabilities: caps }, value) cap.effective = value cap.effective_display = cap.current_display } } return { ...matrix, capabilities: caps } } const DEFAULT_ROUTING: Record = { daily_data_provider: 'tickflow', adj_factor_provider: 'tickflow', minute_data_provider: 'tickflow', depth5_data_provider: 'tickflow', realtime_data_provider: 'tickflow', financial_data_provider: 'tickflow', } /** 单个能力卡: 当前生效提供方 + 候选切换标签。 * candidates 只含当前可提供该能力的源; 未就绪源 (pending) 置灰提示; * 生效源无法提供该能力 (usable=False, 无论档位不足还是源未就绪) 时显示琥珀警示。 */ function CapabilityCard({ cap, pendingKey, onSelect }: { cap: CapabilityRoute pendingKey: string | null onSelect: (field: ProviderField, provider: string) => void }) { const Icon = CAP_ICON[cap.id] ?? Database const busy = (provider: string) => pendingKey === `${cap.field}:${provider}` // 能力中立判定: usable=False 即当前路由的源供不了 (TickFlow 档位不足或插件未就绪同待遇) const unmet = !cap.usable const chipsEmpty = cap.candidates.length === 0 && cap.pending.length === 0 return (
{cap.label}
{cap.desc &&
{cap.desc}
} {/* 当前生效提供方 */}
当前 {unmet ? ( 能力不可用 ) : ( <> {cap.effective_display} )}
{/* 候选标签: 点谁该能力就由谁提供 */}
{cap.candidates.map(c => { const active = cap.current === c.name return ( ) })} {/* 未就绪源: 声明了该能力但依赖/Key 未配好, 置灰并说明原因 */} {cap.pending.map(c => ( {c.display} ))} {chipsEmpty && ( 暂无可用提供方 )}
) } /** 能力路由区 (页面主视图): 每个能力一张卡, 点候选标签即刻切换 (乐观更新) */ function CapabilityRoutingSection() { const qc = useQueryClient() const matrix = useQuery({ queryKey: QK.capabilityMatrix, queryFn: api.capabilityMatrix }) const [pendingKey, setPendingKey] = useState(null) const invalidateRouting = () => { qc.invalidateQueries({ queryKey: QK.capabilityMatrix }) qc.invalidateQueries({ queryKey: QK.preferences }) qc.invalidateQueries({ queryKey: QK.capabilities }) qc.invalidateQueries({ queryKey: QK.quoteStatus }) } /** 切换前先把变更写进矩阵/偏好缓存, 界面零延迟响应; 失败回滚 */ const applyOptimistic = (changes: Partial>) => { const prevMatrix = qc.getQueryData(QK.capabilityMatrix) const prevPrefs = qc.getQueryData(QK.preferences) if (prevMatrix) qc.setQueryData(QK.capabilityMatrix, patchMatrix(prevMatrix, changes)) if (prevPrefs) qc.setQueryData(QK.preferences, { ...prevPrefs, ...changes }) return { prevMatrix, prevPrefs } } const rollback = (ctx: { prevMatrix?: CapabilityMatrix; prevPrefs?: Preferences } | undefined) => { if (ctx?.prevMatrix) qc.setQueryData(QK.capabilityMatrix, ctx.prevMatrix) if (ctx?.prevPrefs) qc.setQueryData(QK.preferences, ctx.prevPrefs) } const routeMut = useMutation({ mutationFn: ({ field, provider }: { field: ProviderField; provider: string }) => // 动态键经运行时字段名收敛为合法偏好键 (字段名来自后端注册表) api.updateDataProviders({ [field]: provider } as Partial>), onMutate: async (v) => { setPendingKey(`${v.field}:${v.provider}`) await qc.cancelQueries({ queryKey: QK.capabilityMatrix }) await qc.cancelQueries({ queryKey: QK.preferences }) return applyOptimistic({ [v.field]: v.provider }) }, onSuccess: (_d, v) => { const cap = matrix.data?.capabilities.find(c => c.field === v.field) toast(`「${cap?.label || v.field}」已切换为 ${displayOfName(matrix.data, v.provider)}`, 'success') }, onError: (e: Error, _v, ctx) => { rollback(ctx) toast(`路由切换失败: ${e.message}`, 'error') }, onSettled: () => { setPendingKey(null) invalidateRouting() }, }) const resetMut = useMutation({ mutationFn: () => api.updateDataProviders(DEFAULT_ROUTING), onMutate: async () => { await qc.cancelQueries({ queryKey: QK.capabilityMatrix }) await qc.cancelQueries({ queryKey: QK.preferences }) return applyOptimistic(DEFAULT_ROUTING) }, onSuccess: () => toast('能力路由已恢复默认', 'success'), onError: (e: Error, _v, ctx) => { rollback(ctx) toast(`恢复失败: ${e.message}`, 'error') }, onSettled: invalidateRouting, }) const list = matrix.data?.capabilities ?? [] const anyCustom = list.some(c => c.current !== c.default) return (

能力路由

{list.length} 个能力
{anyCustom && ( )}

每个能力独立选择提供方 — 点标签即刻生效。选项只列出当前可提供该能力的源 (各源按自身可用性过滤, 详见下方数据源介绍); 未就绪的源置灰提示。

{matrix.isError ? (
能力矩阵加载失败: {(matrix.error as Error)?.message || '未知错误'}
) : matrix.isLoading || list.length === 0 ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : (
{list.map(cap => ( routeMut.mutate({ field, provider })} /> ))}
)}
) } /** 插件 API Key 配置区块 (嵌入插件详情卡, 不再独立成卡)。 * 先探后存: 后端用候选 Key 实探一次, 无效不落盘; secrets.json 优先于 .env。 */ function PluginKeyConfig({ plugin }: { plugin: PluginDataSourceItem }) { const qc = useQueryClient() const [keyInput, setKeyInput] = useState('') const [revealing, setRevealing] = useState(false) const [saved, setSaved] = useState(false) const invalidate = () => { qc.invalidateQueries({ queryKey: QK.dataSources }) qc.invalidateQueries({ queryKey: QK.capabilityMatrix }) qc.invalidateQueries({ queryKey: QK.capabilities }) qc.invalidateQueries({ queryKey: QK.quoteStatus }) } const save = useMutation({ mutationFn: () => api.savePluginKey(plugin.name, keyInput.trim()), onSuccess: (data) => { invalidate() if (data.ok) { setKeyInput('') setSaved(true) setTimeout(() => setSaved(false), 2000) } }, onError: (e: Error) => toast(`保存失败: ${e.message}`, 'error'), }) const clear = useMutation({ mutationFn: () => api.clearPluginKey(plugin.name), onSuccess: (data) => { invalidate() if (data.ok) { toast( data.plugin_available ? '已清除界面配置的 Key(.env 中的同名变量仍然生效)' : 'Key 已清除,插件不再可用', 'success', ) } }, onError: (e: Error) => toast(`清除失败: ${e.message}`, 'error'), }) return (

API Key

{plugin.api_key_env}
{/* 申请说明 + 官网链接 (对齐 TickFlow Key 区话术) */}

{plugin.homepage ? ( <> 在{' '} {plugin.display_name} 官网 {' '}申请获取。 ) : ( <>向该数据源官方申请 API Key。 )} Key 仅存本地 (secrets.json 优先, 环境变量 {plugin.api_key_env} 兜底),不会上传任何第三方,请妥善保管。

{/* 当前状态 */}
状态
{plugin.available ? ( <> 已配置 {/* 生效 Key 脱敏串 (secrets.json 优先, .env 兜底) — 常驻显示, 与 TickFlow Key 一致 */} {plugin.api_key_masked && ( {plugin.api_key_masked} )} ) : ( <> 未配置 {plugin.status} )}
{plugin.available && ( )}
{/* 输入 */}
{ e.preventDefault(); if (keyInput.trim()) save.mutate() }} className="space-y-2" >
{ setKeyInput(e.target.value); if (saved) setSaved(false) }} autoComplete="off" className="w-full px-3 py-2 pr-9 rounded-input bg-base border border-border text-sm font-mono focus:outline-none focus:border-accent transition-colors duration-150 ease-smooth" />
{/* 无效 Key —— 先探后存: 探测失败时不存储 */} {save.data && !save.data.ok && (
{save.data.error || 'Key 无效,未保存'}
)} {save.isError && (
保存失败:{String((save.error as Error).message)}
)}
) } /** 能力芯片: 三态 — 服务中(高亮+勾) / 已适配(灰) / 档位锁定(锁, 仅 TickFlow) */ function CapabilityChips({ caps, servingSet, isTickFlow }: { caps: CapabilityRoute[] servingSet: Set isTickFlow: boolean }) { if (caps.length === 0) return 未声明能力 return (
{caps.map(cap => { const servingNow = servingSet.has(cap.id) const locked = isTickFlow && !cap.tf_available const cls = servingNow ? 'bg-accent/15 text-accent' : locked ? 'bg-warning/8 text-warning/70' : 'bg-elevated/60 text-muted/70' const title = servingNow ? `正在提供「${cap.label}」` : locked ? `TickFlow 需 ${tierReqText(cap.tf_tier)} · 当前档位未解锁` : `已适配「${cap.label}」` return ( {servingNow ? : locked ? : null} {DATASET_LABEL[cap.id] || cap.id} ) })}
) } export function SettingsDataSourcesPanel({ highlight }: { highlight?: string } = {}) { const qc = useQueryClient() const prefs = usePreferences() const sources = useQuery({ queryKey: QK.dataSources, queryFn: api.dataSources }) const matrix = useQuery({ queryKey: QK.capabilityMatrix, queryFn: api.capabilityMatrix }) const [selected, setSelected] = useState('tickflow') // 当前在下方配置的源 name const [confirmDelete, setConfirmDelete] = useState(null) const builtin: DataSourceItem[] = sources.data?.builtin ?? [] const pluginList: PluginDataSourceItem[] = sources.data?.plugins ?? [] const customList: DataSourceItem[] = sources.data?.custom ?? [] const errors = sources.data?.errors ?? [] const activeName = prefs.data?.daily_data_provider || 'tickflow' const pluginItems: DataSourceItem[] = pluginList.map(p => ({ name: p.name, display_name: p.display_name, datasets: p.datasets, })) const allItems = [ ...builtin, ...pluginItems, ...customList, ] const customNames = new Set(customList.map(c => c.name)) const selectedCustom = customList.find(s => s.name === selected) // ===== 各能力当前的有效提供方 (除权 same_as_daily = 跟随日K) ===== // 用于能力芯片"服务中"态与详情卡标识: 路由切换在上方能力路由区, 这里只读展示 const dailyPref = prefs.data?.daily_data_provider || 'tickflow' const adjPref = prefs.data?.adj_factor_provider || 'same_as_daily' const effProvider: Record = { daily: dailyPref, adj_factor: adjPref === 'same_as_daily' ? dailyPref : adjPref, minute: prefs.data?.minute_data_provider || 'tickflow', realtime: prefs.data?.realtime_data_provider || 'tickflow', financial: prefs.data?.financial_data_provider || 'tickflow', } const servingDatasets = (name: string) => Object.entries(effProvider).filter(([, v]) => v === name).map(([k]) => k) const servingSetOf = (name: string) => new Set(servingDatasets(name)) const matrixCaps = matrix.data?.capabilities ?? [] // 数据源集合/插件可用性/档位变化都会改变能力候选集 → 统一连带失效 const invalidateSources = () => { qc.invalidateQueries({ queryKey: QK.dataSources }) qc.invalidateQueries({ queryKey: QK.capabilityMatrix }) qc.invalidateQueries({ queryKey: QK.preferences }) qc.invalidateQueries({ queryKey: QK.capabilities }) qc.invalidateQueries({ queryKey: QK.quoteStatus }) } const reload = useMutation({ mutationFn: api.reloadDataSources, onSuccess: () => { invalidateSources() toast('配置已重新加载', 'success') }, }) const remove = useMutation({ mutationFn: (name: string) => api.deleteDataSource(name), onSuccess: () => { invalidateSources() setSelected('tickflow') setConfirmDelete(null) toast('数据源已删除', 'success') }, }) const switchProvider = useMutation({ mutationFn: async (name: string) => { // 一键套用: 该源适配了哪些数据集就接管哪些, 其余回默认 if (name === 'tickflow') { return api.updateDataProviders(DEFAULT_ROUTING) } const supported = new Set( allItems.find(s => s.name === name)?.datasets ?? [] ) const pick = (dataset: string) => supported.has(dataset) ? name : DEFAULT_ROUTING[`${dataset}_data_provider` as ProviderField] ?? 'tickflow' return api.updateDataProviders({ daily_data_provider: pick('daily'), adj_factor_provider: pick('adj_factor'), realtime_data_provider: pick('realtime'), minute_data_provider: pick('minute'), financial_data_provider: pick('financial'), }) }, onSuccess: (_d, name) => { invalidateSources() const display = allItems.find(s => s.name === name)?.display_name || name toast(`已让「${display}」接管其适配的能力`, 'success') }, onError: (e: Error) => toast(`切换失败: ${e.message}`, 'error'), }) const editExisting = useMutation({ mutationFn: (name: string) => api.dataSource(name), onSuccess: (_data, name) => setSelected(name), }) const installMut = useMutation({ mutationFn: (name: string) => api.installPlugin(name), onSuccess: (data) => { invalidateSources() if (data.install_ok) { toast('插件依赖安装成功', 'success') } else { toast(data.install_message || '安装失败', 'error') } }, onError: (e: Error) => toast(`安装失败: ${e.message}`, 'error'), }) const uninstallMut = useMutation({ mutationFn: (name: string) => api.uninstallPlugin(name), onSuccess: (data) => { invalidateSources() if (data.uninstall_ok) { toast(data.uninstall_message || '已卸载', 'success') } else { toast(data.uninstall_message || '卸载失败', 'error') } }, onError: (e: Error) => toast(`卸载失败: ${e.message}`, 'error'), }) // 插件 name → 状态 (供卡片渲染时判断 available/installing 等) const pluginMap = new Map(pluginList.map(p => [p.name, p])) return (
{/* ===== 上区: 能力路由 (能力为主视图, 点标签切换提供方) ===== */} {/* ===== 下区: 数据源 (源为主视图, 配置/接入) ===== */}

数据源

{sources.data?.config_dir}
{/* 插件化说明 (置顶黄色提示条): 接入自有行情 → 把文档交给 AI */}
数据源已插件化 ,接入自有行情?把文档发给 AI 即可自动接入: docs/custom-data-source.md (自有 HTTP 接口) · docs/plugin-development.md (插件开发)
{/* 数据源卡片 - 横向网格 (配置入口) */}
{allItems.map(item => { const servingSet = servingSetOf(item.name) const isTf = item.name === 'tickflow' if (isTf) { // 名义路由到 tickflow 但当前档位提供不了的能力 → 按锁定态展示, 不算服务中 for (const c of matrixCaps) { if (!c.tf_available) servingSet.delete(c.id) } } const servingCount = servingSet.size const isSelected = selected === item.name const plugin = pluginMap.get(item.name) const pluginUnavailable = plugin && !plugin.available const installing = installMut.isPending && installMut.variables === item.name const uninstalling = uninstallMut.isPending && uninstallMut.variables === item.name const declared = new Set(item.datasets) // TickFlow 展示注册表全量能力 (含档位锁定态); 其余源按声明过滤 const chipCaps = isTf ? matrixCaps : matrixCaps.filter(c => declared.has(c.id)) return (
{ // 未就绪插件仅当支持界面配 Key 时可点开(进详情配置); 其余不可选 if (pluginUnavailable && !plugin?.api_key_env) return setSelected(item.name) // 只有用户自定义源 (YAML) 才进编辑器; tickflow 和插件不可编辑 if (customNames.has(item.name)) { editExisting.mutate(item.name) } }} className={`relative text-left rounded-lg border px-3.5 py-3 transition-all ${ pluginUnavailable && !plugin?.api_key_env ? 'border-border/40 bg-elevated/10 opacity-70' : isSelected ? 'border-accent/50 bg-accent/5 ring-1 ring-accent/20 cursor-pointer' : 'border-border/60 bg-elevated/20 hover:bg-elevated/40 cursor-pointer' }`} > {/* 名称行: 服务点 + 名称 + 档位/第三方标识 */}
0 ? 'bg-accent' : 'bg-transparent border border-muted/40' }`} /> 0 ? 'font-medium text-foreground' : 'text-secondary'}`}> {item.display_name} {isTf && matrix.data?.tickflow_tier && ( )} {!customNames.has(item.name) && ( 第三方 )}
{/* 能力芯片: 服务中 / 已适配 / 档位锁定 */}
{/* 底部: 状态提示 + 操作 */}
{servingCount > 0 ? `服务中 ${servingCount} 项能力` : pluginUnavailable ? (plugin?.runtime === 'none' ? '点击配置 Key' : (plugin?.install_hint || plugin?.status || '')) : ''}
{pluginUnavailable ? ( plugin?.runtime !== 'none' && ( installing ? ( 安装中... ) : ( ) ) ) : ( <> {plugin && plugin?.runtime !== 'none' && ( uninstalling ? ( ) : ( ) )} )}
) })} {/* 新增数据源卡片 */}
{/* 错误提示 */} {errors.length > 0 && (
{errors.map((err, idx) => (
{err.name || err.path}: {err.errors.join('; ')}
))}
)}
芯片: 高亮=服务中 · 灰=已适配 · =需更高 TickFlow 档位 · 单击卡片查看介绍与配置, 点「套用」让该源接管其适配的全部能力
{/* ===== 下方: 选中源的介绍 + 配置 (单一卡片布局) ===== */} {selected === 'tickflow' ? ( 0} matrix={matrix.data} /> ) : selected === '__new__' || customNames.has(selected) ? ( selected === '__new__' ? ( setSelected('tickflow')} onSaved={() => { invalidateSources() setSelected('tickflow') }} activeName={activeName} onActivate={(name) => switchProvider.mutate(name)} /> ) : ( setSelected('tickflow')} onSaved={() => { invalidateSources() // 强制清除该源的详情缓存, 下次编辑重新拉取最新配置 qc.removeQueries({ queryKey: ['data-source-detail', selected] }) }} activeName={activeName} onActivate={(name) => switchProvider.mutate(name)} onDelete={selectedCustom ? () => setConfirmDelete(selected) : undefined} /> ) ) : pluginList.find(x => x.name === selected) ? ( x.name === selected)!} isActive={servingDatasets(selected).length > 0} matrixCaps={matrixCaps} servingSet={servingSetOf(selected)} onSwitch={() => switchProvider.mutate(selected)} switching={switchProvider.isPending} /> ) : null} {/* 删除确认弹窗 */} {confirmDelete && (
setConfirmDelete(null)} />

删除数据源

确认删除「{customList.find(s => s.name === confirmDelete)?.display_name || confirmDelete}」? 该数据源的配置文件将被移除,此操作不可撤销。

)}
) } /** 插件详情: 介绍 + 适配能力 + Key 配置合并为单一卡片 */ function PluginDetail({ plugin, isActive, matrixCaps, servingSet, onSwitch, switching }: { plugin: PluginDataSourceItem isActive: boolean matrixCaps: CapabilityRoute[] servingSet: Set onSwitch: () => void switching: boolean }) { const declared = new Set(plugin.datasets) return (
{/* 介绍 */}

{plugin.display_name}

插件 · {plugin.runtime} 第三方 {isActive && ( 服务中 )}
{plugin.description &&

{plugin.description}

}
declared.has(c.id))} servingSet={servingSet} isTickFlow={false} />
{/* 主体: 操作 + Key 配置 (左) | 能力适配表 (右), 布局对齐 TickFlow 详情 */}
{plugin.available ? ( !isActive && ( ) ) : ( {plugin.status} )}
{/* API Key 配置 (声明了 api_key_env 的插件) */} {plugin.api_key_env && (
)}
{/* 能力适配表: 全部能力 × 该源适配状态 (样式对齐 TickFlow 能力档位表) */}
{matrixCaps.map(cap => { const Icon = CAP_ICON[cap.id] ?? Database const serving = servingSet.has(cap.id) const declaredCap = declared.has(cap.id) return (
{cap.label} {cap.desc}
{serving ? ( 服务中 ) : declaredCap ? ( {plugin.available ? '已适配' : '未就绪'} ) : ( )}
) })}
) } /** TickFlow 详情: 介绍 + 能力档位表 + Key/可用功能左右两栏。 * 检测档位集群 (档位徽章 + ? 说明 + 重新检测 + 可用功能悬停) 挂在 API Key 标题行右侧。 */ function TickFlowDetail({ active, matrix }: { active: boolean; matrix?: CapabilityMatrix }) { const caps = matrix?.capabilities ?? [] const tier = matrix?.tickflow_tier const { data: tfCaps } = useCapabilities() const capEntries = tfCaps ? Object.entries(tfCaps.capabilities) : [] const invalidate = useInvalidateTierRelated() const redetect = useMutation({ mutationFn: () => api.redetectCapabilities(), onSuccess: () => invalidate(), }) // 检测档位集群: API Key 标题行右侧 (检测档位徽章 + 档位说明 + 重检测 + 可用功能悬停) const tierCluster = tier ? (
检测档位 {/* 可用功能: 收进悬停浮层, 不占版面 (能力清单 + 限频)。图标在标题行右侧, 浮层向左展开 */}
可用功能 {capEntries.length} 项
{capEntries.length > 0 ? (
{capEntries.map(([cap, lim]) => (
{CAP_LABELS[cap]?.name ?? cap} {lim.rpm ? `${lim.rpm}/min` : lim.subscribe ? `${lim.subscribe} 订阅` : '—'} {lim.batch ? ` · ${lim.batch}/次` : ''}
))}
) : (
暂无 — 配置 API Key 后自动检测
)}
根据 API Key 自动检测
) : null return (
{/* 介绍 */}

TickFlow

第三方 {active && ( 服务中 )}

默认数据源,每个能力所需订阅档位见下表 — 当前档位未解锁的能力不会出现在上方「能力路由」的选项里。 未单独设置的能力默认由 TickFlow 提供;也可在数据源区接入插件替换任意能力。

{/* 主体: API Key 配置 (左) | 能力档位表 (右) */}
{/* 能力档位表: 各能力所需档位 + 当前档位可用性 */} {caps.length > 0 && (
{caps.map(cap => { const Icon = CAP_ICON[cap.id] ?? Database return (
{cap.label} {cap.desc}
{cap.tf_available ? ( 可用 ) : ( 未解锁 )}
) })}
)}
) }