同 baseUrl 不同 name 的 provider 切换时账号表格累加 bug 分析
背景
在 ServiceProvidersPage.tsx 中,密钥管理页面允许在同一个 baseUrl 下配置多个不同 name 的 provider。理论上:
- 切换 provider a,应只显示 a 的账号
- 切换 provider b,应只显示 b 的账号
- 再切换 a,应仍然只显示 a 的账号
现象
当多个 provider 的 baseUrl 相同但 name 不同时,重复点击导航栏切换,账号表格会不断累加:
- 第一次点 a:显示 a 的账号
- 第二次点 b:表格不替换为 b 的账号,而是显示 a + b 的账号
- 第三次点 a:显示 a + b + a(或更多)的账号
- 每次切换都出现累加,越切越多
而切换到 baseUrl 不同的 provider c 时,c 始终只显示 c 自己的账号,不会出现累加。
现状分析
导航栏点击
ServiceProvidersPage.tsx:1067-1082 的侧边栏点击只更新 selectedName:
{filteredProviders.map((p) => (
<button
key={p.name}
type="button"
onClick={() => setSelectedName(p.name)}
...
/>
))}当前选中 provider
ServiceProvidersPage.tsx:351-354 按 name 查找:
const selectedProvider = useMemo(
() => providers.find((p) => p.name === selectedName) ?? null,
[providers, selectedName]
);表格数据
ServiceProvidersPage.tsx:411-426 直接取当前 provider 的 apiKeyEntries,不做合并:
const filteredEntries = useMemo(() => {
if (!selectedProvider) return [];
let entries = selectedProvider.apiKeyEntries ?? [];
if (search) { ... }
if (filter !== 'all') { ... }
return entries;
}, [selectedProvider, search, filter, testStatuses]);关键观察
- 点击导航栏只触发
setSelectedName,不会重新请求/openai-compatibility selectedProvider始终按name在providers数组中查找filteredEntries直接来自selectedProvider.apiKeyEntries,没有显式累加逻辑- 出现累加时,必然是
providers数组里那个 provider 对象的apiKeyEntries本身已经被污染
根因猜测
按可能性从高到低排序:
猜测 1:后端按 baseUrl 合并返回(概率最高)
/openai-compatibility 接口在后端实现中,把 baseUrl 相同的多个 provider 的 api-key-entries 合并到同一个 provider 节点返回。
证据:
- 只有 baseUrl 相同才会累加,baseUrl 不同时正常
- 前端没有触发新请求,累加来自已存在的内存数据
- 字段
apiKeyEntries长度在切换时仍然增长,与接口一次性返回合并数据相符
后端可能的错误代码模式:
# 错误示例:按 baseUrl 聚合
compatibility_by_baseurl = {}
for provider in openai_providers:
base = provider["base-url"]
if base not in compatibility_by_baseurl:
compatibility_by_baseurl[base] = {
"name": provider["name"],
"base-url": base,
"api-key-entries": []
}
compatibility_by_baseurl[base]["api-key-entries"].extend(
provider.get("api-key-entries", [])
)
return list(compatibility_by_baseurl.values())猜测 2:前端缓存层按 baseUrl 合并(概率中等)
useProviderWorkbench 或 useConfigStore 中,存在按 baseUrl 缓存/合并 provider 的逻辑,使切换时同 baseUrl 的数据被叠加。
useProviderWorkbench.ts:195-227 的 refetch 直接用新数组覆盖:
updateConfigValue('openai-compatibility', openaiResult.value || []);
clearCache('openai-compatibility');如果 openaiResult.value 已经是合并过的数据,那么累加就来自后端。如果代码里有按 baseUrl 去重或合并的逻辑,也会造成同样现象。
猜测 3:状态引用复用(概率较低)
ServiceProvidersPage 的 providers state 被某处错误地 push/concat,而不是整体替换。但从已读代码看:
setProviders(data)在fetchProviders中整体替换setProviders((prev) => prev.map(...))在各 handler 中只替换对应项- 没有任何
setProviders((prev) => [...prev, ...])的累加写法
所以前端 state 层主动累加的可能性较低,更可能来自接口或缓存层已经返回了合并过的数据。
验证方法
打开浏览器 DevTools Network 面板
切换到 a、b、a
查看
/openai-compatibility接口返回:- 如果 b 返回的 JSON 中
api-key-entries已经包含 a 的账号 → 确认是后端在合并 - 如果接口返回正常但表格仍累加 → 检查前端
providers数组引用
- 如果 b 返回的 JSON 中
在
setSelectedName后打 console.log 打印providers:tsxconst handleSidebarClick = (name: string) => { setSelectedName(name); console.log('[providers]', providers.map(p => ({ name: p.name, baseUrl: p.baseUrl, entryCount: p.apiKeyEntries?.length ?? 0 }))); };在
filteredEntries的 useMemo 中打印来源:tsxconsole.log('[filteredEntries]', selectedProvider?.name, entries.length);
修复方案
根本原则
Provider 的唯一身份标识是 name,不是 baseUrl。baseUrl 只是请求目标地址,不是身份标识。同一个 baseUrl 下配置多个不同 name 的 provider 是合法业务场景。
后端修复(推荐)
/openai-compatibility 接口必须按 name 区分 provider,每个 provider 节点只携带自己的 api-key-entries:
# 正确示例:按 name 直接返回
return openai_providers # 保留所有原始节点,不做合并前端防御性修复
数据获取:
providersApi.getOpenAIProviders每次返回完整数组,前端不做合并:tsxsetProviders(data); // 整体替换,不做合并缓存 key:如果需要按 provider 缓存状态(测试结果等),必须用
name做 key,不是baseUrl:tsxconst keyOf = (provider: OpenAIProviderConfig, entry: ApiKeyEntry): string => `${provider.name}__${entry.apiKey}`; // 已正确使用 name数据完整性校验(可选):在
normalizeOpenAIProvider中,如果发现同 baseUrl 但 name 不同导致 entries 数量异常,打 warning 日志。
不推荐的方案
- 按 baseUrl 合并 provider 节点:会丢失 provider 独立配置(prefix、headers、testModel 等)
- 切换时清空表格再加载:不能解决数据源污染问题
- 限制 baseUrl 唯一:影响合法业务场景(同一服务多份配置)
相关文件
ServiceProvidersPage.tsx- 密钥管理页面,导航栏/表格providers.ts- API 客户端,provider 序列化transformers.ts- 数据规范化useProviderWorkbench.ts- Provider 工作台数据流useConfigStore.ts- 全局配置缓存