Skip to content

同 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

tsx
{filteredProviders.map((p) => (
  <button
    key={p.name}
    type="button"
    onClick={() => setSelectedName(p.name)}
    ...
  />
))}

当前选中 provider

ServiceProvidersPage.tsx:351-354 按 name 查找:

tsx
const selectedProvider = useMemo(
  () => providers.find((p) => p.name === selectedName) ?? null,
  [providers, selectedName]
);

表格数据

ServiceProvidersPage.tsx:411-426 直接取当前 provider 的 apiKeyEntries,不做合并:

tsx
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 始终按 nameproviders 数组中查找
  • filteredEntries 直接来自 selectedProvider.apiKeyEntries,没有显式累加逻辑
  • 出现累加时,必然是 providers 数组里那个 provider 对象的 apiKeyEntries 本身已经被污染

根因猜测

按可能性从高到低排序:

猜测 1:后端按 baseUrl 合并返回(概率最高)

/openai-compatibility 接口在后端实现中,把 baseUrl 相同的多个 provider 的 api-key-entries 合并到同一个 provider 节点返回。

证据:

  • 只有 baseUrl 相同才会累加,baseUrl 不同时正常
  • 前端没有触发新请求,累加来自已存在的内存数据
  • 字段 apiKeyEntries 长度在切换时仍然增长,与接口一次性返回合并数据相符

后端可能的错误代码模式:

python
# 错误示例:按 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 合并(概率中等)

useProviderWorkbenchuseConfigStore 中,存在按 baseUrl 缓存/合并 provider 的逻辑,使切换时同 baseUrl 的数据被叠加。

useProviderWorkbench.ts:195-227refetch 直接用新数组覆盖:

tsx
updateConfigValue('openai-compatibility', openaiResult.value || []);
clearCache('openai-compatibility');

如果 openaiResult.value 已经是合并过的数据,那么累加就来自后端。如果代码里有按 baseUrl 去重或合并的逻辑,也会造成同样现象。

猜测 3:状态引用复用(概率较低)

ServiceProvidersPageproviders state 被某处错误地 push/concat,而不是整体替换。但从已读代码看:

  • setProviders(data)fetchProviders 中整体替换
  • setProviders((prev) => prev.map(...)) 在各 handler 中只替换对应项
  • 没有任何 setProviders((prev) => [...prev, ...]) 的累加写法

所以前端 state 层主动累加的可能性较低,更可能来自接口或缓存层已经返回了合并过的数据。

验证方法

  1. 打开浏览器 DevTools Network 面板

  2. 切换到 a、b、a

  3. 查看 /openai-compatibility 接口返回:

    • 如果 b 返回的 JSON 中 api-key-entries 已经包含 a 的账号 → 确认是后端在合并
    • 如果接口返回正常但表格仍累加 → 检查前端 providers 数组引用
  4. setSelectedName 后打 console.log 打印 providers

    tsx
    const handleSidebarClick = (name: string) => {
      setSelectedName(name);
      console.log('[providers]', providers.map(p => ({
        name: p.name,
        baseUrl: p.baseUrl,
        entryCount: p.apiKeyEntries?.length ?? 0
      })));
    };
  5. filteredEntries 的 useMemo 中打印来源

    tsx
    console.log('[filteredEntries]', selectedProvider?.name, entries.length);

修复方案

根本原则

Provider 的唯一身份标识是 name,不是 baseUrl。baseUrl 只是请求目标地址,不是身份标识。同一个 baseUrl 下配置多个不同 name 的 provider 是合法业务场景。

后端修复(推荐)

/openai-compatibility 接口必须按 name 区分 provider,每个 provider 节点只携带自己的 api-key-entries

python
# 正确示例:按 name 直接返回
return openai_providers  # 保留所有原始节点,不做合并

前端防御性修复

  1. 数据获取providersApi.getOpenAIProviders 每次返回完整数组,前端不做合并:

    tsx
    setProviders(data);  // 整体替换,不做合并
  2. 缓存 key:如果需要按 provider 缓存状态(测试结果等),必须用 name 做 key,不是 baseUrl

    tsx
    const keyOf = (provider: OpenAIProviderConfig, entry: ApiKeyEntry): string =>
      `${provider.name}__${entry.apiKey}`;  // 已正确使用 name
  3. 数据完整性校验(可选):在 normalizeOpenAIProvider 中,如果发现同 baseUrl 但 name 不同导致 entries 数量异常,打 warning 日志。

不推荐的方案

  • 按 baseUrl 合并 provider 节点:会丢失 provider 独立配置(prefix、headers、testModel 等)
  • 切换时清空表格再加载:不能解决数据源污染问题
  • 限制 baseUrl 唯一:影响合法业务场景(同一服务多份配置)

相关文件