Skip to content

ServiceProvidersPage 批量导入导出方案

背景

ServiceProvidersPage.tsx 当前没有批量导入导出 API Key 的功能。用户需要手动逐个添加 API Key,效率低。需要实现两种导入导出方式,业务逻辑相同,仅交互形式不同。

功能概述

提供两套交互,共享同一套核心逻辑:

操作交互形式方向
复制粘贴板导出
粘贴粘贴板导入
导出下载 JSON 文件导出
导入上传 JSON 文件导入

使用场景

  • 用户在同一 provider 下管理多个 API Key Entry
  • 导出/复制:在当前 provider 的密钥表格中勾选若干条目,点击"复制"或"导出"
  • 导入/粘贴:在当前 provider 下,从粘贴板粘贴或上传文件,批量添加 API Key Entry
  • 导入导出都是针对当前选中的 providerapiKeyEntries

设计决策

问题决策
API Key 是否脱敏不脱敏,明文导出,自用场景
冲突处理跳过 + 提示,同 apiKey 视为同一 entry,跳过并告知用户
导入导出粒度支持部分选择,可勾选部分 entry 进行导入导出
字段范围只需要 apiKeybaseUrl,足够了

导出格式

简洁的 JSON 数组格式,每个 entry 包含 apiKeybaseUrltype 三个字段:

json
[
    { "apiKey": "sk-xxxxxxxxxxxx", "baseUrl": "https://api.example.com/v1", "type": "openai-compatibility" },
    { "apiKey": "sk-yyyyyyyyyyyy", "baseUrl": "https://api.example.com/v1", "type": "openai-compatibility" },
    { "apiKey": "sk-zzzzzzzzzzzz", "baseUrl": "https://api.example.com/v1", "type": "openai-compatibility" }
]

格式说明

字段必填说明
apiKeyAPI Key 明文
baseUrl对应的 baseUrl
typeProvider 类型,当前固定为 "openai-compatibility",用于识别数据来源和未来扩展

格式校验

导入时通过检查数组第一个元素的字段来识别数据来源:

typescript
function isValidImportData(data: unknown): boolean {
  if (!Array.isArray(data) || data.length === 0) return false;
  const first = data[0];
  return (
    typeof first === 'object' &&
    first !== null &&
    'apiKey' in first &&
    'baseUrl' in first &&
    'type' in first
  );
}

核心逻辑

导出/复制 共用逻辑

输入:选中的 entries[]
输出:格式化 JSON 字符串

步骤 1: 获取选中的 apiKeyEntries(从 selectedEntryIndices 映射)
步骤 2: 构建导出数据对象(只取 apiKey 和 baseUrl 字段)
步骤 3: JSON.stringify(data, null, 2)

导入/粘贴 共用逻辑

输入:JSON 字符串
输出:合并后的 apiKeyEntries[]

步骤 1: JSON.parse(text) → 失败则报错"不是有效的 JSON"
步骤 2: 检查是数组且非空 → 否则报错"没有可导入的条目"
步骤 3: 检查第一个元素包含 apiKey、baseUrl、type 字段 → 否则报错"不是有效的导出数据"
步骤 4: 遍历 entries,校验 apiKey 非空
步骤 5: 合并到当前 provider 的 apiKeyEntries:
        - 已存在的 apiKey → 跳过,记录跳过数量
        - 新的 apiKey → 追加
步骤 6: 通过 providersApi.saveOpenAIProviders() 保存
步骤 7: 刷新列表,toast 提示导入结果(成功 N 条,跳过 M 条)

冲突处理

跳过已存在的 apiKey,不覆盖。导入时如果发现当前 provider 已有相同 apiKey 的 entry,跳过该条目,最终 toast 提示"成功导入 N 条,跳过 M 条(已存在)"。

proxyUrl / authIndex 说明

导出格式不包含 proxyUrlauthIndex,这是预期行为。导入等同于新增 entry,只携带 apiKeybaseUrl 信息。

交互细节

复制(粘贴板导出)

  • 按钮位置:批量操作栏,当有选中条目时显示
  • 按钮文案复制 ()
  • 行为:构建 JSON → copyToClipboard(json) → toast "已复制 N 条记录到粘贴板"
  • 依赖:已有 copyToClipboard

粘贴(粘贴板导入)

  • 按钮位置:批量操作栏,始终显示
  • 按钮文案粘贴
  • 行为
    • 优先尝试 navigator.clipboard.readText()(HTTPS / localhost 环境)
    • 降级:弹出 Modal,内含 <textarea> 让用户手动粘贴 JSON,点击"确认导入"后执行导入
    • 解析校验 → 合并 → 保存 → toast
  • 依赖:需要在 clipboard.ts 中添加 readFromClipboard() 函数;需要新增一个简单的 Modal/Dialog 组件(或复用项目已有的)

导出(文件下载)

  • 按钮位置:批量操作栏,当有选中条目时显示
  • 按钮文案导出 ()
  • 行为:构建 JSON → 创建 Blob → downloadBlob() → toast "已导出 N 条记录"
  • 文件名{providerName}-api-keys-{timestamp}.json
  • 依赖:已有 downloadBlob

导入(文件上传)

  • 按钮位置:批量操作栏,始终显示
  • 按钮文案导入
  • 行为:触发隐藏的 <input type="file" accept=".json"> → 读取文件内容 → 解析校验 → 合并 → 保存 → toast
  • 依赖:原生 File API,无需额外工具

UI 布局

在现有批量操作栏中,按钮分组排列:

[ 全选 ] | [ 删除选中 (3) ] | [ 复制 (3) ] [ 导出 (3) ] [ 粘贴 ] [ 导入 ] | [ Move Selected (3) ]
  • 复制/导出 按钮只在有选中条目时显示
  • 粘贴/导入 按钮始终显示
  • 按钮样式与现有批量操作按钮一致

实现涉及的文件

文件改动
ServiceProvidersPage.tsx添加 4 个按钮和处理函数,核心导入导出逻辑
ServiceProvidersPage.module.scss按钮样式(如有需要)
clipboard.ts添加 readFromClipboard() 函数
download.ts已有,无需改动
Modal/Dialog 组件新增或复用已有组件,用于 HTTP 环境下的手动粘贴弹窗
zh-CN.json添加中文翻译
en.json添加英文翻译

实现步骤

  1. clipboard.ts 中添加 readFromClipboard() 工具函数,内部判断 navigator.clipboard 是否可用,不可用返回 null 由调用方降级
  2. ServiceProvidersPage.tsx 中:
    • 提取 buildExportJson(entries) 共用函数:构建导出 JSON 字符串
    • 提取 parseImportJson(text) 共用函数:解析校验导入 JSON
    • 提取 mergeEntries(existing, imported) 共用函数:合并 entries(跳过已存在的 apiKey,返回 {added, skipped} 计数)
    • 添加 handleCopy / handlePaste / handleExport / handleImport 四个处理函数
    • handlePaste 中:优先 readFromClipboard(),返回 null 时弹出 Modal 让用户手动粘贴
    • 在批量操作栏添加 4 个按钮
  3. 添加粘贴降级 Modal 组件(textarea + 确认/取消按钮),可在 ServiceProvidersPage.tsx 内部定义或单独文件
  4. 添加 i18n 翻译文案
  5. 测试:导出 → 清空 → 导入 验证数据完整性

测试场景

场景预期结果
选中 3 条 → 复制 → 粘贴板有正确 JSON
粘贴板有正确 JSON → 粘贴 → 新 entry 追加
粘贴板有正确 JSON → 粘贴 → 同 apiKey 跳过,提示"成功 N 条,跳过 M 条"
粘贴板内容不是 JSON → 粘贴 → 错误提示
HTTP 环境 → 点击粘贴 → 弹出 Modal textarea → 手动粘贴 → 确认导入
选中 3 条 → 导出 → 下载 JSON 文件
上传正确 JSON 文件 → 导入 → 合并成功
上传非 JSON 文件 → 导入 → 错误提示
未选中任何条目 → 复制/导出按钮不显示
粘贴/导入按钮始终可见