mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
fix(settings): 数据源「套用」接管该源声明的全部能力
原实现手写 5 个偏好字段 (daily/adj_factor/realtime/minute/financial), 漏掉 depth5 与 full_minute —— 点「套用」后这两个能力仍停在旧数据源。 改为由后端能力矩阵驱动: 遍历矩阵中每个可路由能力 (id 为数据集名、 field 为偏好字段), 该源声明即接管, 未声明回落默认源; 矩阵未加载时 fail-closed 抛错。能力清单不再在前端重复维护。 验证: 新增 7 个用例 (含"必须覆盖全部字段"防回归); pnpm build 通过。
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { CapabilityRoute, ProviderField } from '@/lib/api'
|
||||
import { DEFAULT_ROUTING, buildProviderRouting } from '@/lib/provider-routing'
|
||||
|
||||
/** 与后端能力注册表同形的路由能力 (id = 数据集名, field = 偏好字段)。 */
|
||||
const CAPS: Pick<CapabilityRoute, 'id' | 'field'>[] = [
|
||||
{ id: 'daily', field: 'daily_data_provider' },
|
||||
{ id: 'adj_factor', field: 'adj_factor_provider' },
|
||||
{ id: 'realtime', field: 'realtime_data_provider' },
|
||||
{ id: 'minute', field: 'minute_data_provider' },
|
||||
{ id: 'depth5', field: 'depth5_data_provider' },
|
||||
{ id: 'financial', field: 'financial_data_provider' },
|
||||
{ id: 'full_minute', field: 'full_minute_data_provider' },
|
||||
]
|
||||
|
||||
const ALL_FIELDS: ProviderField[] = [
|
||||
'daily_data_provider',
|
||||
'adj_factor_provider',
|
||||
'realtime_data_provider',
|
||||
'minute_data_provider',
|
||||
'depth5_data_provider',
|
||||
'financial_data_provider',
|
||||
'full_minute_data_provider',
|
||||
]
|
||||
|
||||
const DEMO = 'demo_source'
|
||||
|
||||
describe('buildProviderRouting', () => {
|
||||
it('接管声明过的全部能力, 包括 depth5 与 full_minute', () => {
|
||||
const routing = buildProviderRouting(
|
||||
DEMO,
|
||||
['realtime', 'depth5', 'daily', 'minute', 'full_minute'],
|
||||
CAPS,
|
||||
)
|
||||
expect(routing.daily_data_provider).toBe(DEMO)
|
||||
expect(routing.realtime_data_provider).toBe(DEMO)
|
||||
expect(routing.minute_data_provider).toBe(DEMO)
|
||||
expect(routing.depth5_data_provider).toBe(DEMO)
|
||||
expect(routing.full_minute_data_provider).toBe(DEMO)
|
||||
})
|
||||
|
||||
it('未声明的能力回落默认源, 不残留上一个数据源', () => {
|
||||
const routing = buildProviderRouting(
|
||||
DEMO,
|
||||
['realtime', 'depth5', 'daily', 'minute', 'full_minute'],
|
||||
CAPS,
|
||||
)
|
||||
expect(routing.adj_factor_provider).toBe('tickflow')
|
||||
expect(routing.financial_data_provider).toBe('tickflow')
|
||||
})
|
||||
|
||||
it('每个可路由字段都被覆盖 (防止再漏能力)', () => {
|
||||
const routing = buildProviderRouting(DEMO, [], CAPS)
|
||||
for (const field of ALL_FIELDS) {
|
||||
expect(routing[field], `missing field ${field}`).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('切换源时旧源的能力全部被覆盖', () => {
|
||||
// 声明 6 项 (无 financial) 的源
|
||||
const routing = buildProviderRouting(
|
||||
DEMO,
|
||||
['realtime', 'depth5', 'daily', 'adj_factor', 'minute', 'full_minute'],
|
||||
CAPS,
|
||||
)
|
||||
expect(routing.adj_factor_provider).toBe(DEMO)
|
||||
expect(routing.financial_data_provider).toBe('tickflow')
|
||||
expect(Object.keys(routing).sort()).toEqual([...ALL_FIELDS].sort())
|
||||
})
|
||||
|
||||
it('能力矩阵未加载时 fail-closed 抛错', () => {
|
||||
expect(() => buildProviderRouting(DEMO, ['daily'], [])).toThrow(/能力矩阵未加载/)
|
||||
})
|
||||
|
||||
it('不可路由能力 (field=null) 被跳过', () => {
|
||||
const routing = buildProviderRouting(DEMO, ['daily'], [
|
||||
{ id: 'daily', field: 'daily_data_provider' },
|
||||
{ id: 'tickflow_only', field: null },
|
||||
])
|
||||
expect(routing.daily_data_provider).toBe(DEMO)
|
||||
expect(Object.keys(routing).sort()).toEqual([...ALL_FIELDS].sort())
|
||||
})
|
||||
|
||||
it('默认路由覆盖全部字段且均为 tickflow', () => {
|
||||
expect(Object.keys(DEFAULT_ROUTING).sort()).toEqual([...ALL_FIELDS].sort())
|
||||
expect(new Set(Object.values(DEFAULT_ROUTING))).toEqual(new Set(['tickflow']))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { CapabilityRoute, ProviderField } from '@/lib/api'
|
||||
|
||||
/** 全部可路由偏好字段的默认生效源。 */
|
||||
export const DEFAULT_ROUTING: Record<ProviderField, string> = {
|
||||
daily_data_provider: 'tickflow',
|
||||
adj_factor_provider: 'tickflow',
|
||||
minute_data_provider: 'tickflow',
|
||||
full_minute_data_provider: 'tickflow',
|
||||
depth5_data_provider: 'tickflow',
|
||||
realtime_data_provider: 'tickflow',
|
||||
financial_data_provider: 'tickflow',
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算「一键套用」的目标路由: 该源声明了哪个数据集, 就接管对应能力;
|
||||
* 未声明的能力回落默认源, 避免残留上一个数据源的配置。
|
||||
*
|
||||
* 能力清单取自后端能力矩阵 (dataset id 与能力 field 的单一权威), 不在前端
|
||||
* 手写字段列表 —— 手写清单会漏掉新增能力 (depth5 / full_minute 就这样被漏过,
|
||||
* 点「套用」后这两个能力仍停在旧数据源)。
|
||||
*
|
||||
* 矩阵未加载时 fail-closed 抛错, 而不是把全部路由重置成默认源。
|
||||
*/
|
||||
export function buildProviderRouting(
|
||||
provider: string,
|
||||
datasets: readonly string[],
|
||||
capabilities: readonly Pick<CapabilityRoute, 'id' | 'field'>[],
|
||||
): Record<ProviderField, string> {
|
||||
if (!capabilities.length) {
|
||||
throw new Error('能力矩阵未加载, 无法套用数据源, 请稍后重试')
|
||||
}
|
||||
const supported = new Set(datasets)
|
||||
const routing: Record<ProviderField, string> = { ...DEFAULT_ROUTING }
|
||||
for (const cap of capabilities) {
|
||||
if (!cap.field) continue // 不可路由能力 (仅 TickFlow 提供) 没有偏好字段
|
||||
routing[cap.field] = supported.has(cap.id) ? provider : DEFAULT_ROUTING[cap.field]
|
||||
}
|
||||
return routing
|
||||
}
|
||||
@@ -41,6 +41,7 @@ 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 { DEFAULT_ROUTING, buildProviderRouting } from '@/lib/provider-routing'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { DataSourceEditor } from './DataSourceEditor'
|
||||
import { TickFlowKeySection, TierHelpPopover, useInvalidateTierRelated } from './Keys'
|
||||
@@ -146,16 +147,6 @@ function patchMatrix(
|
||||
return { ...matrix, capabilities: caps }
|
||||
}
|
||||
|
||||
const DEFAULT_ROUTING: Record<ProviderField, string> = {
|
||||
daily_data_provider: 'tickflow',
|
||||
adj_factor_provider: 'tickflow',
|
||||
minute_data_provider: 'tickflow',
|
||||
full_minute_data_provider: 'tickflow',
|
||||
depth5_data_provider: 'tickflow',
|
||||
realtime_data_provider: 'tickflow',
|
||||
financial_data_provider: 'tickflow',
|
||||
}
|
||||
|
||||
/** 单个能力卡: 当前生效提供方 + 候选切换标签。
|
||||
* candidates 只含当前可提供该能力的源; 未就绪源 (pending) 置灰提示;
|
||||
* 生效源无法提供该能力 (usable=False, 无论档位不足还是源未就绪) 时显示琥珀警示。 */
|
||||
@@ -685,18 +676,13 @@ export function SettingsDataSourcesPanel({ highlight }: { highlight?: string } =
|
||||
if (name === 'tickflow') {
|
||||
return api.updateDataProviders(DEFAULT_ROUTING)
|
||||
}
|
||||
const supported = new Set(
|
||||
allItems.find(s => s.name === name)?.datasets ?? []
|
||||
return api.updateDataProviders(
|
||||
buildProviderRouting(
|
||||
name,
|
||||
allItems.find(s => s.name === name)?.datasets ?? [],
|
||||
matrix.data?.capabilities ?? [],
|
||||
),
|
||||
)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user