[
  {
    "path": ".github/workflows/deploy-docs.yml",
    "content": "name: 部署文档\n\non:\n  push:\n    branches:\n      # 确保这是你正在使用的分支名称\n      - dev\n    paths: # <<<<<< 新增：路径过滤\n      - 'docs/docs/**'          # 匹配 /docs/docs/ 目录下所有文档内容和配置 (包括 .vuepress/、config.js 等)\n      - 'docs/package.json'     # 匹配 /docs/package.json 文件 (因为依赖定义在这里)\n      - 'docs/pnpm-lock.json'   # 匹配 /docs/pnpm-lock.json 文件 (因为依赖版本锁定在这里)\n\npermissions:\n  contents: write\n\njobs:\n  deploy-gh-pages:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: docs # 所有 run 命令将在此目录下执行\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          # 如果你的文档需要 Git 子模块，取消注释下一行\n          # submodules: true\n\n      - name: 安装 pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 8\n\n      - name: 设置 Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: pnpm\n\n      - name: 安装依赖\n        run: pnpm install --no-frozen-lockfile\n\n      - name: 构建文档\n        env:\n          NODE_OPTIONS: --max_old_space_size=8192\n        run: |-\n          # 因为 working-directory 是 docs，所以这里执行的是 docs/package.json 中的 docs:build\n          # 且构建输出在 /docs/docs/.vuepress/dist (相对于仓库根目录)\n          # 或者在当前工作目录 /docs 内的 docs/.vuepress/dist\n          pnpm run docs:build\n          # 创建 .nojekyll 文件，防止 GitHub Pages 尝试解析 Jekyll\n          # 该文件将位于 /docs/docs/.vuepress/dist/.nojekyll\n          > docs/.vuepress/dist/.nojekyll\n\n      - name: 部署文档\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          branch: gh-pages # 部署到的目标分支\n          # 指定要部署的文件夹。相对于 GitHub 仓库根目录。\n          # 您的 VitePress 文档内容在 /docs/docs/ 下，构建输出通常在 /docs/docs/.vuepress/dist\n          folder: docs/docs/.vuepress/dist"
  },
  {
    "path": ".github/workflows/submit.yml",
    "content": "name: \"Submit to Web Store\"\non:\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Cache pnpm modules\n        uses: actions/cache@v3\n        with:\n          path: ~/.pnpm-store\n          key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-\n      - uses: pnpm/action-setup@v2.2.4\n        with:\n          version: latest\n          run_install: true\n      - name: Use Node.js 16.x\n        uses: actions/setup-node@v3.4.1\n        with:\n          node-version: 16.x\n          cache: \"pnpm\"\n      - name: Build the extension\n        run: pnpm build\n      - name: Package the extension into a zip artifact\n        run: pnpm package\n      - name: Browser Platform Publish\n        uses: PlasmoHQ/bpp@v3\n        with:\n          keys: ${{ secrets.SUBMIT_KEYS }}\n          artifact: build/chrome-mv3-prod.zip\n"
  },
  {
    "path": ".gitignore",
    "content": "\n# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\nout/\nbuild/\ndist/\n\n# plasmo\n.plasmo\n\n# typescript\n.tsbuildinfo\n\n.idea\n\n# 文档项目的依赖和构建产物\n/docs/node_modules/\n/docs/docs/.vuepress/dist/\n/docs/docs/.vuepress/.cache/\n/docs/docs/.vuepress/.temp/\n\n# 其他可能的文档生成器特定忽略项 (例如 Docusaurus 的 build 目录)\n# /docs/build/"
  },
  {
    "path": ".prettierrc.mjs",
    "content": "/**\n * @type {import('prettier').Options}\n */\nexport default {\n  printWidth: 80,\n  tabWidth: 2,\n  useTabs: false,\n  semi: false,\n  singleQuote: false,\n  trailingComma: \"none\",\n  bracketSpacing: true,\n  bracketSameLine: true,\n  plugins: [\"@ianvs/prettier-plugin-sort-imports\"],\n  importOrder: [\n    \"<BUILTIN_MODULES>\", // Node.js built-in modules\n    \"<THIRD_PARTY_MODULES>\", // Imports not matched by other special words or groups.\n    \"\", // Empty line\n    \"^@plasmo/(.*)$\",\n    \"\",\n    \"^@plasmohq/(.*)$\",\n    \"\",\n    \"^~(.*)$\",\n    \"\",\n    \"^[./]\"\n  ]\n}\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "## 用户定义\n- 当前项目使用 Plasmo v0.90.5 开发\n- 前端技术栈（Tailwind CSS v3、Headless UI等）\n- 当技术文档不确定时，应当使用 mcp 工具 context7 进行搜索\n\n### 项目介绍\n```\n## 介绍\n\n目前市面上有太多 ai-api 中转站点，每次查看余额和支持模型列表等信息都非常麻烦，需要逐个登录查看。\n\n本插件可以便捷的对基于https://github.com/songquanpeng/one-api和[new-api](https://github.com/QuantumNous/new-api)等部署的 Ai 中转站账号进行整合管理。\n\n### 功能\n\n- 自动识别中转站点，自动创建系统访问 token 并添加到插件的站点列表中\n- 每个站点可添加多个账号\n- 账号的余额、使用日志进行查看\n- 令牌(key)查看与管理\n- 站点支持模型信息和渠道查看\n- 插件无需联网\n\n### 未来支持\n\n- 模型降智测试\n- webdav 数据备份\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2025 fxaxg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <img src=\"assets/icon.png\" alt=\"One API Hub Logo\" width=\"128\" height=\"128\">\n  \n  # 中转站管理器 - One API Hub\n  \n  **一个开源的浏览器插件，聚合管理所有中转站账号的余额、模型和密钥，告别繁琐登录。**\n  \n  [![Version](https://img.shields.io/badge/version-0.0.3-blue.svg)](https://github.com/fxaxg/one-api-hub)\n  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n  [![Plasmo](https://img.shields.io/badge/plasmo-v0.90.5-purple.svg)](https://plasmo.com)\n  [![React](https://img.shields.io/badge/react-18.2.0-61dafb.svg)](https://reactjs.org)\n  [![TypeScript](https://img.shields.io/badge/typescript-5.3.3-blue.svg)](https://typescriptlang.org)\n  [![Tailwind CSS](https://img.shields.io/badge/tailwindcss-3.4.17-38bdf8.svg)](https://tailwindcss.com)\n\n   **[文档教程](https://fxaxg.github.io/one-api-hub/) | [常见问题](https://fxaxg.github.io/one-api-hub/faq.html)**\n  \n</div>\n\n---\n\n## 📖 介绍\n\n目前市面上有太多 AI-API 中转站点，每次查看余额和支持模型列表等信息都非常麻烦，需要逐个登录查看。\n\n本插件可以便捷的对基于以下项目的AI 中转站账号进行整合管理：\n- [one-api](https://github.com/songquanpeng/one-api)\n- [new-api](https://github.com/QuantumNous/new-api) \n- [Veloera](https://github.com/Veloera/Veloera)\n- [one-hub](https://github.com/MartialBE/one-hub)\n- [done-hub](https://github.com/deanxv/done-hub)\n\n\n\n## ✨ 功能特性\n\n- 🔍 **自动识别中转站点** - 自动创建系统访问 token 并添加到插件的站点列表中\n- 💰 **自动识别中转站充值比例** - 智能解析站点配置信息\n- 👥 **多账号管理** - 每个站点可添加多个账号\n- 📊 **余额与日志查看** - 账号的余额、使用日志一目了然\n- 🔑 **令牌(key)管理** - 便捷的密钥查看与管理\n- 🤖 **模型信息查看** - 站点支持模型信息和渠道查看\n- 🔒 **完全离线** - 插件无需联网，保护隐私安全\n\n## 🖥️ 截图展示\n\n![软件截图](./docs/docs/static/image/app_show.png)\n\n## 🚀 安装使用\n\n### Chrome 应用商店（推荐）\n[⬇️ 前往下载](https://chromewebstore.google.com/detail/%E4%B8%AD%E8%BD%AC%E7%AB%99%E7%AE%A1%E7%90%86%E5%99%A8-one-api-hub/eobdoeafpplhhhjfkinnlkljbkijpobd)\n\n<!-- ### 手动安装\n1. 下载最新版本的扩展包\n2. 打开 Chrome 浏览器，进入 `chrome://extensions/`\n3. 开启 \"开发者模式\"\n4. 点击 \"加载已解压的扩展程序\"\n5. 选择解压后的扩展文件夹 -->\n\n## 🛠️ 开发指南\n\n### 环境要求\n- Node.js 18+\n- npm 或 pnpm\n\n### 本地开发\n\n```bash\n# 克隆项目\ngit clone https://github.com/username/one-api-hub.git\ncd one-api-hub\n\n# 安装依赖\npnpm install\n# 或者\nnpm install\n\n# 启动开发服务器\npnpm dev\n# 或者\nnpm run dev\n```\n\n然后在浏览器中加载 `build/chrome-mv3-dev` 目录作为扩展程序。\n\n### 构建生产版本\n\n```bash\npnpm build\n# 或者 \nnpm run build\n```\n\n这将在 `build` 目录中创建生产版本的扩展包。\n\n## 🔮 未来支持\n\n- 🧪 **模型降智测试** - 自动化模型性能测试\n- ☁️ **WebDAV 数据备份** - 云端数据同步与备份\n\n\n## 👥 贡献者（不分先后）\n\n感谢以下贡献者对项目的支持：\n\n- [@qixing-jk](https://github.com/qixing-jk)\n- [@JianKang-Li](https://github.com/JianKang-Li)\n\n\n## 🏗️ 技术栈\n\n- **框架**: [Plasmo](https://plasmo.com) v0.90.5\n- **UI 库**: [React](https://reactjs.org) 18.2.0\n- **样式**: [Tailwind CSS](https://tailwindcss.com) v3.4.17\n- **组件**: [Headless UI](https://headlessui.com)\n- **图标**: [Heroicons](https://heroicons.com)\n- **状态管理**: [Zustand](https://zustand-demo.pmnd.rs)\n- **类型检查**: [TypeScript](https://typescriptlang.org) 5.3.3\n\n\n## 📄 许可证\n\n本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。\n\n\n## 🙏 致谢\n- [Plasmo](https://plasmo.com) - 现代化的浏览器扩展开发框架\n\n---\n\n<div align=\"center\">\n  <strong>⭐ 如果这个项目对你有帮助，请考虑给它一个星标！</strong>\n</div>"
  },
  {
    "path": "background.ts",
    "content": "import {\n  autoRefreshService,\n  handleAutoRefreshMessage\n} from \"./services/autoRefreshService\"\n\n// 管理临时窗口的 Map\nconst tempWindows = new Map<string, number>()\n\n// 插件启动时初始化自动刷新服务\nchrome.runtime.onStartup.addListener(async () => {\n  console.log(\"[Background] 插件启动，初始化自动刷新服务\")\n  await autoRefreshService.initialize()\n})\n\n// 插件安装时初始化自动刷新服务\nchrome.runtime.onInstalled.addListener(async () => {\n  console.log(\"[Background] 插件安装/更新，初始化自动刷新服务\")\n  await autoRefreshService.initialize()\n})\n\n// 处理来自 popup 的消息\nchrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {\n  if (request.action === \"openTempWindow\") {\n    handleOpenTempWindow(request, sendResponse)\n    return true // 保持异步响应通道\n  }\n\n  if (request.action === \"closeTempWindow\") {\n    handleCloseTempWindow(request, sendResponse)\n    return true\n  }\n\n  if (request.action === \"autoDetectSite\") {\n    handleAutoDetectSite(request, sendResponse)\n    return true\n  }\n\n  // 处理自动刷新相关消息\n  if (\n    (request.action && request.action.startsWith(\"autoRefresh\")) ||\n    [\n      \"setupAutoRefresh\",\n      \"refreshNow\",\n      \"stopAutoRefresh\",\n      \"updateAutoRefreshSettings\",\n      \"getAutoRefreshStatus\"\n    ].includes(request.action)\n  ) {\n    handleAutoRefreshMessage(request, sendResponse)\n    return true\n  }\n})\n\n// 打开临时窗口访问指定站点\nasync function handleOpenTempWindow(request: any, sendResponse: Function) {\n  try {\n    const { url, requestId } = request\n\n    // 创建新窗口\n    const window = await chrome.windows.create({\n      url: url,\n      type: \"popup\",\n      width: 800,\n      height: 600,\n      focused: false\n    })\n\n    if (window.id) {\n      // 记录窗口ID\n      tempWindows.set(requestId, window.id)\n      sendResponse({ success: true, windowId: window.id })\n    } else {\n      sendResponse({ success: false, error: \"无法创建窗口\" })\n    }\n  } catch (error) {\n    sendResponse({ success: false, error: error.message })\n  }\n}\n\n// 关闭临时窗口\nasync function handleCloseTempWindow(request: any, sendResponse: Function) {\n  try {\n    const { requestId } = request\n    const windowId = tempWindows.get(requestId)\n\n    if (windowId) {\n      await chrome.windows.remove(windowId)\n      tempWindows.delete(requestId)\n    }\n\n    sendResponse({ success: true })\n  } catch (error) {\n    sendResponse({ success: false, error: error.message })\n  }\n}\n\n// 自动检测站点信息\nasync function handleAutoDetectSite(request: any, sendResponse: Function) {\n  const { url, requestId } = request\n\n  try {\n    // 1. 打开临时窗口\n    const window = await chrome.windows.create({\n      url: url,\n      type: \"popup\",\n      width: 800,\n      height: 600,\n      focused: false\n    })\n\n    if (!window.id || !window.tabs?.[0]?.id) {\n      throw new Error(\"无法创建窗口或获取标签页\")\n    }\n\n    const windowId = window.id\n    const tabId = window.tabs[0].id\n\n    // 记录窗口\n    tempWindows.set(requestId, windowId)\n\n    // 2. 等待页面加载完成\n    await waitForTabComplete(tabId)\n\n    // 3. 通过 content script 获取用户信息\n    const userResponse = await chrome.tabs.sendMessage(tabId, {\n      action: \"getUserFromLocalStorage\",\n      url: url\n    })\n    console.log(userResponse.error)\n\n    if (!userResponse.success) {\n      throw new Error(userResponse.error)\n    }\n\n    // 4. 关闭临时窗口\n    await chrome.windows.remove(windowId)\n    tempWindows.delete(requestId)\n\n    // 5. 返回结果\n    sendResponse({\n      success: true,\n      data: {\n        userId: userResponse.data.userId,\n        user: userResponse.data.user\n      }\n    })\n  } catch (error) {\n    // 清理窗口\n    const windowId = tempWindows.get(requestId)\n    if (windowId) {\n      try {\n        await chrome.windows.remove(windowId)\n        tempWindows.delete(requestId)\n      } catch (cleanupError) {\n        console.log(\"清理窗口失败:\", cleanupError)\n      }\n    }\n\n    sendResponse({ success: false, error: error.message })\n  }\n}\n\n// 等待标签页加载完成\nfunction waitForTabComplete(tabId: number): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const timeout = setTimeout(() => {\n      reject(new Error(\"页面加载超时\"))\n    }, 10000) // 10秒超时\n\n    const checkStatus = () => {\n      chrome.tabs.get(tabId, (tab) => {\n        if (chrome.runtime.lastError) {\n          clearTimeout(timeout)\n          reject(new Error(chrome.runtime.lastError.message))\n          return\n        }\n\n        if (tab.status === \"complete\") {\n          clearTimeout(timeout)\n          // 再等待一秒确保页面完全加载\n          setTimeout(resolve, 1000)\n        } else {\n          setTimeout(checkStatus, 100)\n        }\n      })\n    }\n\n    checkStatus()\n  })\n}\n\n// 监听窗口关闭事件，清理记录\nchrome.windows.onRemoved.addListener((windowId) => {\n  for (const [requestId, storedWindowId] of tempWindows.entries()) {\n    if (storedWindowId === windowId) {\n      tempWindows.delete(requestId)\n      break\n    }\n  }\n})\n"
  },
  {
    "path": "components/AccountList.tsx",
    "content": "import { ChevronUpIcon, ChevronDownIcon, ChartBarIcon, CpuChipIcon, EllipsisHorizontalIcon, DocumentDuplicateIcon, ChartPieIcon, PencilIcon, TrashIcon, ArrowPathIcon, InboxIcon, KeyIcon } from \"@heroicons/react/24/outline\"\nimport { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'\nimport CountUp from \"react-countup\"\nimport { UI_CONSTANTS, HEALTH_STATUS_MAP } from \"../constants/ui\"\nimport { getCurrencySymbol } from \"../utils/formatters\"\nimport type { DisplaySiteData } from \"../types\"\nimport { useState, useCallback, useRef, useEffect } from 'react'\nimport Tooltip from './Tooltip'\nimport DelAccountDialog from './DelAccountDialog'\nimport CopyKeyDialog from './CopyKeyDialog'\n\ntype SortField = 'name' | 'balance' | 'consumption'\ntype SortOrder = 'asc' | 'desc'\n\ninterface AccountListProps {\n  // 数据\n  sites: DisplaySiteData[]\n  currencyType: 'USD' | 'CNY'\n  \n  // 排序状态\n  sortField: SortField\n  sortOrder: SortOrder\n  \n  // 动画相关\n  isInitialLoad: boolean\n  prevBalances: { [id: string]: { USD: number, CNY: number } }\n  \n  // 刷新状态\n  refreshingAccountId?: string | null\n  \n  // 事件处理\n  onSort: (field: SortField) => void\n  onAddAccount: () => void\n  onRefreshAccount?: (site: DisplaySiteData) => Promise<void>\n  onCopyUrl?: (site: DisplaySiteData) => void\n  onViewUsage?: (site: DisplaySiteData) => void\n  onViewModels?: (site: DisplaySiteData) => void\n  onEditAccount?: (site: DisplaySiteData) => void\n  onDeleteAccount?: (site: DisplaySiteData) => void\n  onViewKeys?: (site: DisplaySiteData) => void\n}\n\nexport default function AccountList({\n  sites,\n  currencyType,\n  sortField,\n  sortOrder,\n  isInitialLoad,\n  prevBalances,\n  refreshingAccountId,\n  onSort,\n  onAddAccount,\n  onRefreshAccount,\n  onCopyUrl,\n  onViewUsage,\n  onViewModels,\n  onEditAccount,\n  onDeleteAccount,\n  onViewKeys\n}: AccountListProps) {\n  const [hoveredSiteId, setHoveredSiteId] = useState<string | null>(null)\n  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n  const [deleteDialogAccount, setDeleteDialogAccount] = useState<DisplaySiteData | null>(null)\n  const [copyKeyDialogAccount, setCopyKeyDialogAccount] = useState<DisplaySiteData | null>(null)\n\n  // 防抖的 hover 处理\n  const handleMouseEnter = useCallback((siteId: string) => {\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current)\n    }\n    hoverTimeoutRef.current = setTimeout(() => {\n      setHoveredSiteId(siteId)\n    }, 50) // 100ms 防抖延迟\n  }, [])\n\n  const handleMouseLeave = useCallback(() => {\n    if (hoverTimeoutRef.current) {\n      clearTimeout(hoverTimeoutRef.current)\n    }\n    hoverTimeoutRef.current = setTimeout(() => {\n      setHoveredSiteId(null)\n    }, 0) // 不需要离开时的延迟\n  }, [])\n\n  // 清理定时器\n  useEffect(() => {\n    return () => {\n      if (hoverTimeoutRef.current) {\n        clearTimeout(hoverTimeoutRef.current)\n      }\n    }\n  }, [])\n\n  const copyToClipboard = async (text: string) => {\n    try {\n      await navigator.clipboard.writeText(text)\n    } catch (err) {\n      console.error('Failed to copy:', err)\n    }\n  }\n\n  const handleCopyUrl = (site: DisplaySiteData) => {\n    copyToClipboard(site.baseUrl)\n    onCopyUrl?.(site)\n  }\n\n  const handleCopyKey = (site: DisplaySiteData) => {\n    setCopyKeyDialogAccount(site)\n  }\n\n  const handleRefreshAccount = async (site: DisplaySiteData) => {\n    if (onRefreshAccount) {\n      try {\n        await onRefreshAccount(site)\n      } catch (error) {\n        console.error('刷新账号失败:', error)\n      }\n    }\n  }\n  if (sites.length === 0) {\n    return (\n      <div className=\"px-6 py-12 text-center\">\n        <InboxIcon className=\"w-16 h-16 text-gray-200 mx-auto mb-4\" />\n        <p className=\"text-gray-500 text-sm mb-4\">暂无站点账号</p>\n        <button \n          onClick={onAddAccount}\n          className=\"px-6 py-2.5 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors shadow-sm\"\n        >\n          添加第一个站点账号\n        </button>\n      </div>\n    )\n  }\n\n  const renderSortButton = (field: SortField, label: string) => (\n    <button\n      onClick={() => onSort(field)}\n      className=\"flex items-center space-x-1 text-xs font-medium text-gray-500 hover:text-gray-700 transition-colors\"\n    >\n      <span>{label}</span>\n      {sortField === field && (\n        sortOrder === 'asc' ? \n          <ChevronUpIcon className=\"w-3 h-3\" /> : \n          <ChevronDownIcon className=\"w-3 h-3\" />\n      )}\n    </button>\n  )\n\n  return (\n    <div className=\"flex flex-col\">\n      {/* 表头 */}\n      <div className=\"px-5 py-3 bg-gray-50 border-b border-gray-100 sticky top-0 z-10\">\n        <div className=\"flex items-center space-x-4\">\n          <div className=\"flex-1\">\n            {renderSortButton('name', '账号')}\n          </div>\n          <div className=\"text-right flex-shrink-0\">\n            <div className=\"flex items-center space-x-1\">\n              {renderSortButton('balance', '余额')}\n              <span className=\"text-xs text-gray-400\">/</span>\n              {renderSortButton('consumption', '今日消耗')}\n            </div>\n          </div>\n        </div>\n      </div>\n      \n      {/* 账号列表 */}\n      {sites.map((site) => (\n        <div \n          key={site.id} \n          className=\"px-5 py-4 border-b border-gray-50 hover:bg-gray-25 transition-colors relative group\"\n          onMouseEnter={() => handleMouseEnter(site.id)}\n          onMouseLeave={handleMouseLeave}\n        >\n          <div className=\"flex items-center space-x-4\">\n            {/* 站点信息 */}\n            <div className=\"flex items-center space-x-3 flex-1 min-w-0\">\n              <div className=\"flex-1 min-w-0\">\n                <div className=\"flex items-center space-x-2 mb-0.5\">\n                  {/* 站点状态指示器 */}\n                  <div className={`w-2 h-2 rounded-full flex-shrink-0 ${\n                    HEALTH_STATUS_MAP[site.healthStatus]?.color || UI_CONSTANTS.STYLES.STATUS_INDICATOR.UNKNOWN\n                  }`}></div>\n                  <div className=\"font-medium text-gray-900 text-sm truncate\">\n                    <a\n                      href={site.baseUrl}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                    >\n                      {site.name}\n                    </a>\n                  </div>\n                </div>\n                <div className=\"text-xs text-gray-500 truncate ml-4\">{site.username}</div>\n              </div>\n            </div>\n            \n            {/* 按钮组 - 只在 hover 时显示 */}\n            {hoveredSiteId === site.id && (\n              <div className=\"flex items-center space-x-2 flex-shrink-0\">\n                {/* 刷新按钮 */}\n                <Tooltip content=\"刷新账号\" position=\"top\">\n                  <button\n                    onClick={() => handleRefreshAccount(site)}\n                    className=\"flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 transition-colors\"\n                    disabled={refreshingAccountId === site.id}\n                  >\n                    <ArrowPathIcon \n                      className={`w-4 h-4 text-gray-500 ${\n                        refreshingAccountId === site.id ? 'animate-spin' : ''\n                      }`} \n                    />\n                  </button>\n                </Tooltip>\n\n                {/* 复制下拉菜单 */}\n                <Menu as=\"div\" className=\"relative\">\n                  <Tooltip content=\"复制\" position=\"top\">\n                    <MenuButton className=\"flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 transition-colors\">\n                      <DocumentDuplicateIcon className=\"w-4 h-4 text-gray-500\" />\n                    </MenuButton>\n                  </Tooltip>\n                  <MenuItems \n                    anchor=\"bottom end\"\n                    className=\"z-50 w-32 bg-white rounded-lg shadow-lg border border-gray-200 py-1 focus:outline-none [--anchor-gap:4px] [--anchor-padding:8px]\"\n                  >\n                    <MenuItem>\n                      <button\n                        onClick={() => handleCopyUrl(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-gray-700 hover:text-gray-900 data-focus:bg-gray-50 flex items-center space-x-2\"\n                      >\n                        <DocumentDuplicateIcon className=\"w-4 h-4\" />\n                        <span>复制 URL</span>\n                      </button>\n                    </MenuItem>\n                    <MenuItem>\n                      <button\n                        onClick={() => handleCopyKey(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-gray-700 hover:text-gray-900 data-focus:bg-gray-50 flex items-center space-x-2\"\n                      >\n                        <DocumentDuplicateIcon className=\"w-4 h-4\" />\n                        <span>复制密钥</span>\n                      </button>\n                    </MenuItem>\n                    <hr />\n                    <MenuItem>\n                      <button\n                        onClick={() => onViewKeys?.(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-gray-700 hover:text-gray-900 data-focus:bg-gray-50 flex items-center space-x-2\"\n                      >\n                        <KeyIcon className=\"w-4 h-4\" />\n                        <span>管理密钥</span>\n                      </button>\n                    </MenuItem>\n                  </MenuItems>\n                </Menu>\n\n                {/* 更多下拉菜单 */}\n                <Menu as=\"div\" className=\"relative\">\n                    <MenuButton className=\"flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 transition-colors\">\n                      <EllipsisHorizontalIcon className=\"w-4 h-4 text-gray-500\" />\n                    </MenuButton>\n                  <MenuItems \n                    anchor=\"bottom end\"\n                    className=\"z-50 w-24 bg-white rounded-lg shadow-lg border border-gray-200 py-1 focus:outline-none [--anchor-gap:4px] [--anchor-padding:8px]\"\n                  >\n                    <MenuItem>\n                      <button\n                        onClick={() => onViewModels?.(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-gray-700 hover:text-gray-900 data-focus:bg-gray-50 flex items-center space-x-2\"\n                      >\n                        <CpuChipIcon className=\"w-4 h-4\" />\n                        <span>模型</span>\n                      </button>\n                    </MenuItem>\n                    <MenuItem>\n                      <button\n                        onClick={() => onViewUsage?.(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-gray-700 hover:text-gray-900 data-focus:bg-gray-50 flex items-center space-x-2\"\n                      >\n                        <ChartPieIcon className=\"w-4 h-4\" />\n                        <span>用量</span>\n                      </button>\n                    </MenuItem>\n                    <hr />\n                    <MenuItem>\n                      <button\n                        onClick={() => onEditAccount?.(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-gray-700 hover:text-gray-900 data-focus:bg-gray-50 flex items-center space-x-2\"\n                      >\n                        <PencilIcon className=\"w-4 h-4\" />\n                        <span>编辑</span>\n                      </button>\n                    </MenuItem>\n                    <MenuItem>\n                      <button\n                        onClick={() => setDeleteDialogAccount(site)}\n                        className=\"w-full px-3 py-2 text-left text-sm text-red-600 hover:text-red-700 data-focus:bg-red-50 flex items-center space-x-2\"\n                      >\n                        <TrashIcon className=\"w-4 h-4\" />\n                        <span>删除</span>\n                      </button>\n                    </MenuItem>\n                  </MenuItems>\n                </Menu>\n              </div>\n            )}\n            \n            {/* 余额和统计 */}\n            <div className=\"text-right flex-shrink-0\">\n              <div className=\"font-semibold text-gray-900 text-lg mb-0.5\">\n                {getCurrencySymbol(currencyType)}\n                <CountUp\n                  start={isInitialLoad ? 0 : (prevBalances[site.id]?.[currencyType] || 0)}\n                  end={site.balance[currencyType]}\n                  duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.SLOW_DURATION : UI_CONSTANTS.ANIMATION.FAST_DURATION}\n                  decimals={2}\n                  preserveValue\n                />\n              </div>\n              <div className={`text-xs ${site.todayConsumption[currencyType] > 0 ? 'text-green-500' : 'text-gray-400'}`}>\n                -{getCurrencySymbol(currencyType)}\n                <CountUp\n                  start={isInitialLoad ? 0 : 0}\n                  end={site.todayConsumption[currencyType]}\n                  duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.SLOW_DURATION : UI_CONSTANTS.ANIMATION.FAST_DURATION}\n                  decimals={2}\n                  preserveValue\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n      ))}\n      \n      {/* 删除账号确认对话框 */}\n      <DelAccountDialog\n        isOpen={deleteDialogAccount !== null}\n        onClose={() => setDeleteDialogAccount(null)}\n        account={deleteDialogAccount}\n        onDeleted={() => onDeleteAccount?.(deleteDialogAccount!)}\n      />\n\n      {/* 复制密钥对话框 */}\n      <CopyKeyDialog\n        isOpen={copyKeyDialogAccount !== null}\n        onClose={() => setCopyKeyDialogAccount(null)}\n        account={copyKeyDialogAccount}\n      />\n    </div>\n  )\n}"
  },
  {
    "path": "components/ActionButtons.tsx",
    "content": "import { PlusIcon, KeyIcon, CpuChipIcon } from \"@heroicons/react/24/outline\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport Tooltip from \"./Tooltip\"\n\ninterface ActionButtonsProps {\n  onAddAccount: () => void\n  onViewKeys: () => void\n  onViewModels: () => void\n}\n\nexport default function ActionButtons({ onAddAccount, onViewKeys, onViewModels }: ActionButtonsProps) {\n  return (\n    <div className=\"px-5 py-4 bg-gray-50/50\">\n      <div className=\"flex space-x-2\">\n        <button \n          onClick={onAddAccount}\n          className={UI_CONSTANTS.STYLES.BUTTON.PRIMARY}\n        >\n          <PlusIcon className=\"w-4 h-4\" />\n          <span>新增账号</span>\n        </button>\n        <Tooltip content=\"密钥管理\">\n          <button \n            onClick={onViewKeys}\n            className={UI_CONSTANTS.STYLES.BUTTON.SECONDARY}\n          >\n            <KeyIcon className=\"w-4 h-4\" />\n          </button>\n        </Tooltip>\n        <Tooltip content=\"模型列表\">\n          <button \n            onClick={onViewModels}\n            className={UI_CONSTANTS.STYLES.BUTTON.SECONDARY}\n          >\n            <CpuChipIcon className=\"w-4 h-4\" />\n          </button>\n        </Tooltip>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "components/AddAccountDialog.tsx",
    "content": "import { useState, useEffect, Fragment } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from \"@headlessui/react\"\nimport { GlobeAltIcon, XMarkIcon, SparklesIcon, UserIcon, KeyIcon, EyeIcon, EyeSlashIcon, CurrencyDollarIcon } from \"@heroicons/react/24/outline\"\nimport { autoDetectAccount, validateAndSaveAccount, extractDomainPrefix, isValidExchangeRate } from \"../services/accountOperations\"\nimport AutoDetectErrorAlert from \"./AutoDetectErrorAlert\"\nimport type { AutoDetectError } from \"../utils/autoDetectUtils\"\n\ninterface AddAccountDialogProps {\n  isOpen: boolean\n  onClose: () => void\n}\n\nexport default function AddAccountDialog({ isOpen, onClose }: AddAccountDialogProps) {\n  const [url, setUrl] = useState(\"\")\n  const [isDetecting, setIsDetecting] = useState(false)\n  const [siteName, setSiteName] = useState(\"\")\n  const [username, setUsername] = useState(\"\")\n  const [accessToken, setAccessToken] = useState(\"\")\n  const [userId, setUserId] = useState(\"\")\n  const [isDetected, setIsDetected] = useState(false)\n  const [isSaving, setIsSaving] = useState(false)\n  const [showAccessToken, setShowAccessToken] = useState(false)\n  const [detectionError, setDetectionError] = useState<AutoDetectError | null>(null)\n  const [showManualForm, setShowManualForm] = useState(false)\n  const [exchangeRate, setExchangeRate] = useState(\"\")\n  const [currentTabUrl, setCurrentTabUrl] = useState<string | null>(null)\n\n\n  useEffect(() => {\n    if (isOpen) {\n      // 重置状态\n      setIsDetected(false)\n      setSiteName(\"\")\n      setUsername(\"\")\n      setAccessToken(\"\")\n      setUserId(\"\")\n      setShowAccessToken(false)\n      setDetectionError(null)\n      setShowManualForm(false)\n      setExchangeRate(\"\")\n      setCurrentTabUrl(null)\n      setUrl(\"\")\n      \n      // 获取当前标签页的 URL 作为初始参考\n      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {\n        if (tabs[0]?.url) {\n          try {\n            const urlObj = new URL(tabs[0].url)\n            const baseUrl = `${urlObj.protocol}//${urlObj.host}`\n            // 如果站点不是以http开头，则不处理（可能为空白页）\n            if (!baseUrl.startsWith('http')) {\n              return\n            }\n            setCurrentTabUrl(baseUrl)\n            // 设置站点名称为域名前缀，但不自动填入URL\n            const domainPrefix = extractDomainPrefix(urlObj.hostname)\n            setSiteName(domainPrefix)\n          } catch (error) {\n            console.log('无法解析 URL:', error)\n            setCurrentTabUrl(null)\n            setSiteName(\"\")\n          }\n        }\n      })\n    }\n  }, [isOpen])\n\n  // 处理点击当前标签页 URL\n  const handleUseCurrentTabUrl = () => {\n    if (currentTabUrl) {\n      setUrl(currentTabUrl)\n    }\n  }\n\n  const handleAutoDetect = async () => {\n    if (!url.trim()) {\n      return\n    }\n\n    setIsDetecting(true)\n    setDetectionError(null)\n    \n    try {\n      const result = await autoDetectAccount(url.trim())\n      \n      if (!result.success) {\n        setDetectionError(result.detailedError || null)\n        setShowManualForm(true)\n        return\n      }\n\n      if (result.data) {\n        // 更新表单数据\n        setUsername(result.data.username)\n        setAccessToken(result.data.accessToken)\n        setUserId(result.data.userId)\n        \n        // 设置充值比例默认值\n        if (result.data.exchangeRate) {\n          setExchangeRate(result.data.exchangeRate.toString())\n          console.log('获取到默认充值比例:', result.data.exchangeRate)\n        } else {\n          setExchangeRate(\"\") // 如果没有获取到，设置为空\n          console.log('未获取到默认充值比例，设置为空')\n        }\n        \n        setIsDetected(true)\n        \n        console.log('自动识别成功:', { \n          username: result.data.username, \n          siteName, \n          exchangeRate: result.data.exchangeRate \n        })\n      }\n    } catch (error) {\n      console.error('自动识别失败:', error)\n      const errorMessage = error instanceof Error ? error.message : '未知错误'\n      // 使用通用错误处理\n      setDetectionError({\n        type: 'unknown' as any,\n        message: `自动识别失败: ${errorMessage}`,\n        helpDocUrl: '#'\n      })\n      setShowManualForm(true) // 识别失败后显示手动表单\n    } finally {\n      setIsDetecting(false)\n    }\n  }\n\n\n  const handleSaveAccount = async () => {\n    setIsSaving(true)\n    \n    try {\n      await toast.promise(\n        validateAndSaveAccount(\n          url.trim(),\n          siteName.trim(),\n          username.trim(),\n          accessToken.trim(),\n          userId.trim(),\n          exchangeRate\n        ),\n        {\n          loading: '正在添加账号...',\n          success: (result) => {\n            if (result.success) {\n              onClose()\n              return `账号 ${siteName} 添加成功!`\n            } else {\n              throw new Error(result.error || '保存失败')\n            }\n          },\n          error: (err) => {\n            const errorMsg = err.message || '添加失败'\n            return `添加失败: ${errorMsg}`\n          },\n        }\n      )\n    } catch (error) {\n      console.error('保存账号失败:', error)\n    } finally {\n      setIsSaving(false)\n    }\n  }\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault()\n    if (isDetected || showManualForm) {\n      handleSaveAccount()\n    } else {\n      handleAutoDetect()\n    }\n  }\n\n  return (\n    <Transition show={isOpen} as={Fragment}>\n      <Dialog\n        onClose={onClose}\n        className=\"relative z-50\"\n      >\n        {/* 背景遮罩动画 */}\n        <TransitionChild\n          as={Fragment}\n          enter=\"ease-out duration-300\"\n          enterFrom=\"opacity-0\"\n          enterTo=\"opacity-100\"\n          leave=\"ease-in duration-200\"\n          leaveFrom=\"opacity-100\"\n          leaveTo=\"opacity-0\"\n        >\n          <div className=\"fixed inset-0 bg-black/30 backdrop-blur-sm\" aria-hidden=\"true\" />\n        </TransitionChild>\n        \n        {/* 居中容器 - 针对插件优化 */}\n        <div className=\"fixed inset-0 flex items-center justify-center p-2\">\n          {/* 弹窗面板动画 */}\n          <TransitionChild\n            as={Fragment}\n            enter=\"ease-out duration-300\"\n            enterFrom=\"opacity-0 scale-95 translate-y-4\"\n            enterTo=\"opacity-100 scale-100 translate-y-0\"\n            leave=\"ease-in duration-200\"\n            leaveFrom=\"opacity-100 scale-100 translate-y-0\"\n            leaveTo=\"opacity-0 scale-95 translate-y-4\"\n          >\n            <DialogPanel className=\"w-full max-w-sm bg-white rounded-lg shadow-xl transform transition-all max-h-[90vh] overflow-y-auto\">\n              {/* 头部 */}\n              <div className=\"flex items-center justify-between p-4 border-b border-gray-100\">\n                <div className=\"flex items-center space-x-3\">\n                  <div className=\"w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center\">\n                    <SparklesIcon className=\"w-4 h-4 text-white\" />\n                  </div>\n                  <DialogTitle className=\"text-lg font-semibold text-gray-900\">\n                    新增账号\n                  </DialogTitle>\n                </div>\n                <button\n                  onClick={onClose}\n                  className=\"p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors\"\n                >\n                  <XMarkIcon className=\"w-5 h-5\" />\n                </button>\n              </div>\n\n              {/* 内容区域 */}\n              <div className=\"p-4\">\n                <form onSubmit={handleSubmit} className=\"space-y-6\">\n                  {/* 识别错误提示 */}\n                  {detectionError && (\n                    <AutoDetectErrorAlert \n                      error={detectionError}\n                      siteUrl={url}\n                    />\n                  )}\n\n                  {/* URL 输入框 */}\n                  <div>\n                    <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                      站点地址\n                    </label>\n                    <div className=\"relative\">\n                      <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                        <GlobeAltIcon className=\"h-5 w-5 text-gray-400\" />\n                      </div>\n                      <input\n                        type=\"url\"\n                        value={url}\n                        onChange={(e) => {\n                          const inputUrl = e.target.value\n                          \n                          // 当用户输入 URL 时，提取协议和主机部分\n                          if (inputUrl.trim()) {\n                            try {\n                              const urlObj = new URL(inputUrl)\n                              // 只保留协议和主机部分，不带路径\n                              const baseUrl = `${urlObj.protocol}//${urlObj.host}`\n                              setUrl(baseUrl)\n                              \n                              // 自动更新站点名称\n                              const domainPrefix = extractDomainPrefix(urlObj.hostname)\n                              setSiteName(domainPrefix)\n                            } catch (error) {\n                              // 如果 URL 格式不完整，先保存用户输入，但尝试提取域名\n                              setUrl(inputUrl)\n                              const match = inputUrl.match(/\\/\\/([^\\/]+)/)\n                              if (match) {\n                                const domainPrefix = extractDomainPrefix(match[1])\n                                setSiteName(domainPrefix)\n                              }\n                            }\n                          } else {\n                            setUrl(\"\")\n                            setSiteName(\"\")\n                          }\n                        }}\n                        placeholder=\"https://example.com\"\n                        className=\"block w-full pl-10 pr-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors\"\n                        required\n                        disabled={isDetected}\n                      />\n                      {url && (\n                        <button\n                          type=\"button\"\n                          onClick={() => setUrl('')}\n                          className=\"absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors\"\n                          disabled={isDetected}\n                        >\n                          <XMarkIcon className=\"h-4 w-4\" />\n                        </button>\n                      )}\n                    </div>\n                    <p className=\"mt-2 text-xs text-gray-500\">\n                      请输入 One API 或 New API 站点的完整地址\n                    </p>\n                    {/* 当前标签页 URL 提示 */}\n                    <Transition\n                      show={!!(currentTabUrl && !url)}\n                      as={Fragment}\n                    >\n                      <TransitionChild\n                        as=\"div\"\n                        enter=\"ease-out duration-500 delay-500\"\n                        enterFrom=\"opacity-0 translate-y-3 scale-90\"\n                        enterTo=\"opacity-100 translate-y-0 scale-100\"\n                        leave=\"ease-in duration-200\"\n                        leaveFrom=\"opacity-100 translate-y-0 scale-100\"\n                        leaveTo=\"opacity-0 translate-y-2 scale-95\"\n                        className=\"mt-2\"\n                      >\n                        <button\n                          type=\"button\"\n                          onClick={handleUseCurrentTabUrl}\n                          className=\"inline-flex items-center px-3 py-1.5 text-xs bg-blue-50 text-blue-700 border border-blue-200 rounded-md hover:bg-blue-100 hover:border-blue-300 transition-all duration-200 transform hover:scale-105 active:scale-95 shadow-sm hover:shadow-md\"\n                        >\n                          <GlobeAltIcon className=\"w-3 h-3 mr-1.5 animate-pulse\" />\n                          <span>使用当前: {currentTabUrl && new URL(currentTabUrl).host}</span>\n                        </button>\n                      </TransitionChild>\n                    </Transition>\n                  </div>\n\n\n                  {/* 识别成功后的表单或手动添加表单 */}\n                  {(isDetected || showManualForm) && (\n                    <>\n                      {/* 网站名称 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                          网站名称\n                        </label>\n                        <div className=\"relative\">\n                          <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                            <GlobeAltIcon className=\"h-5 w-5 text-gray-400\" />\n                          </div>\n                          <input\n                            type=\"text\"\n                            value={siteName}\n                            onChange={(e) => setSiteName(e.target.value)}\n                            placeholder=\"example.com\"\n                            className=\"block w-full pl-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors\"\n                            required\n                          />\n                        </div>\n                      </div>\n\n                      {/* 用户名 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                          用户名\n                        </label>\n                        <div className=\"relative\">\n                          <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                            <UserIcon className=\"h-5 w-5 text-gray-400\" />\n                          </div>\n                          <input\n                            type=\"text\"\n                            value={username}\n                            onChange={(e) => setUsername(e.target.value)}\n                            placeholder=\"用户名\"\n                            className=\"block w-full pl-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors\"\n                            required\n                          />\n                        </div>\n                      </div>\n\n                      {/* 用户 ID */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                          用户 ID\n                        </label>\n                        <div className=\"relative\">\n                          <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                            <span className=\"text-gray-400 font-mono text-sm\">#</span>\n                          </div>\n                          <input\n                            type=\"number\"\n                            value={userId}\n                            onChange={(e) => setUserId(e.target.value)}\n                            placeholder=\"用户 ID (数字)\"\n                            className=\"block w-full pl-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors\"\n                            required\n                          />\n                        </div>\n                      </div>\n\n                      {/* 访问令牌 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                          访问令牌\n                        </label>\n                        <div className=\"relative\">\n                          <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                            <KeyIcon className=\"h-5 w-5 text-gray-400\" />\n                          </div>\n                          <input\n                            type={showAccessToken ? \"text\" : \"password\"}\n                            value={accessToken}\n                            onChange={(e) => setAccessToken(e.target.value)}\n                            placeholder=\"访问令牌\"\n                            className=\"block w-full pl-10 pr-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors\"\n                            required\n                          />\n                          <button\n                            type=\"button\"\n                            onClick={() => setShowAccessToken(!showAccessToken)}\n                            className=\"absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors\"\n                          >\n                            {showAccessToken ? (\n                              <EyeSlashIcon className=\"h-4 w-4\" />\n                            ) : (\n                              <EyeIcon className=\"h-4 w-4\" />\n                            )}\n                          </button>\n                        </div>\n                      </div>\n\n                      {/* 充值金额比例 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                          充值金额比例 (CNY/USD)\n                        </label>\n                        <div className=\"relative\">\n                          <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                            <CurrencyDollarIcon className=\"h-5 w-5 text-gray-400\" />\n                          </div>\n                          <input\n                            type=\"number\"\n                            step=\"0.1\"\n                            min=\"0.1\"\n                            max=\"100\"\n                            value={exchangeRate}\n                            onChange={(e) => setExchangeRate(e.target.value)}\n                            placeholder=\"请输入充值比例\"\n                            className={`block w-full pl-10 py-3 border rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 transition-colors ${\n                              isValidExchangeRate(exchangeRate) \n                                ? 'border-gray-200 focus:ring-blue-500 focus:border-transparent' \n                                : 'border-red-300 focus:ring-red-500 focus:border-red-500'\n                            }`}\n                            required\n                          />\n                          <div className=\"absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none\">\n                            <span className=\"text-sm text-gray-500\">CNY</span>\n                          </div>\n                        </div>\n                        <p className=\"mt-1 text-xs text-gray-500\">\n                          表示充值 1 美元需要多少人民币。系统会尝试自动获取，如未获取到请手动填写\n                        </p>\n                        {!isValidExchangeRate(exchangeRate) && exchangeRate && (\n                          <p className=\"mt-1 text-xs text-red-600\">\n                            请输入有效的汇率 (0.1 - 100)\n                          </p>\n                        )}\n                      </div>\n                    </>\n                  )}\n\n                  {/* 按钮组 */}\n                  <div className=\"flex space-x-3 pt-2\">\n                    <button\n                      type=\"button\"\n                      onClick={onClose}\n                      className=\"flex-1 px-4 py-2.5 text-sm font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500\"\n                    >\n                      取消\n                    </button>\n                    {isDetected ? (\n                      <button\n                        type=\"submit\"\n                        disabled={!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim() || !isValidExchangeRate(exchangeRate) || isSaving}\n                        className=\"flex-1 flex items-center justify-center space-x-2 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm\"\n                      >\n                        {isSaving ? (\n                          <>\n                            <div className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n                            <span>保存中...</span>\n                          </>\n                        ) : (\n                          <>\n                            <SparklesIcon className=\"w-4 h-4\" />\n                            <span>确认添加</span>\n                          </>\n                        )}\n                      </button>\n                    ) : showManualForm ? (\n                      <button\n                        type=\"submit\"\n                        disabled={!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim() || !isValidExchangeRate(exchangeRate) || isSaving}\n                        className=\"flex-1 flex items-center justify-center space-x-2 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm\"\n                      >\n                        {isSaving ? (\n                          <>\n                            <div className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n                            <span>保存中...</span>\n                          </>\n                        ) : (\n                          <>\n                            <SparklesIcon className=\"w-4 h-4\" />\n                            <span>手动添加</span>\n                          </>\n                        )}\n                      </button>\n                    ) : (\n                      <button\n                        type=\"submit\"\n                        disabled={!url.trim() || isDetecting}\n                        className=\"flex-1 flex items-center justify-center space-x-2 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-indigo-600 rounded-lg hover:from-blue-600 hover:to-indigo-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm\"\n                      >\n                        {isDetecting ? (\n                          <>\n                            <div className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n                            <span>识别中...</span>\n                          </>\n                        ) : (\n                          <>\n                            <SparklesIcon className=\"w-4 h-4\" />\n                            <span>自动识别</span>\n                          </>\n                        )}\n                      </button>\n                    )}\n                  </div>\n                  \n                  {/* 手动添加按钮 - 在自动识别失败后显示 */}\n                  {!isDetected && !showManualForm && detectionError && (\n                    <div className=\"pt-2\">\n                      <button\n                        type=\"button\"\n                        onClick={() => setShowManualForm(true)}\n                        className=\"w-full flex items-center justify-center space-x-2 px-4 py-2.5 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500\"\n                      >\n                        <UserIcon className=\"w-4 h-4\" />\n                        <span>手动添加账号信息</span>\n                      </button>\n                    </div>\n                  )}\n                </form>\n              </div>\n\n              {/* 提示信息 */}\n              <div className=\"px-4 pb-4\">\n                <div className=\"bg-blue-50 border border-blue-100 rounded-lg p-3\">\n                  <div className=\"flex\">\n                    <div className=\"flex-shrink-0\">\n                      <SparklesIcon className=\"h-5 w-5 text-blue-400\" />\n                    </div>\n                    <div className=\"ml-3\">\n                      <h3 className=\"text-xs font-medium text-blue-800\">\n                        {isDetected ? '账号信息确认' : showManualForm ? '手动添加' : '自动识别'}\n                      </h3>\n                      <div className=\"mt-1 text-xs text-blue-700\">\n                        <p>\n                          {isDetected \n                            ? '请确认账号信息无误后点击\"确认添加\"按钮。'\n                            : showManualForm\n                            ? '请手动填写账号信息。账号将被安全地保存在本地存储中。'\n                            : '请先在目标站点进行登录，插件将自动检测站点类型，并自动获取访问令牌。'\n                          }\n                        </p>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </DialogPanel>\n          </TransitionChild>\n        </div>\n      </Dialog>\n    </Transition>\n  )\n}"
  },
  {
    "path": "components/AddTokenDialog.tsx",
    "content": "import { useState, useEffect, Fragment } from 'react'\nimport { Dialog, Transition, Switch } from '@headlessui/react'\nimport { \n  XMarkIcon, \n  KeyIcon,\n  ExclamationTriangleIcon\n} from '@heroicons/react/24/outline'\nimport { \n  fetchAvailableModels, \n  fetchUserGroups, \n  createApiToken,\n  fetchTokenById,\n  updateApiToken,\n  type GroupInfo,\n  type CreateTokenRequest,\n  type ApiToken\n} from '../services/apiService'\nimport { UI_CONSTANTS } from '../constants/ui'\nimport toast from 'react-hot-toast'\n\ninterface AddTokenDialogProps {\n  isOpen: boolean\n  onClose: () => void\n  availableAccounts: Array<{\n    id: string\n    name: string\n    baseUrl: string\n    userId: number\n    token: string\n  }>\n  preSelectedAccountId?: string | null\n  editingToken?: ApiToken & { accountName: string } | null\n}\n\ninterface FormData {\n  accountId: string\n  name: string\n  quota: string\n  expiredTime: string\n  unlimitedQuota: boolean\n  modelLimitsEnabled: boolean\n  modelLimits: string[]\n  allowIps: string\n  group: string\n}\n\nexport default function AddTokenDialog({ isOpen, onClose, availableAccounts, preSelectedAccountId, editingToken }: AddTokenDialogProps) {\n  const [formData, setFormData] = useState<FormData>({\n    accountId: '',\n    name: '',\n    quota: '',\n    expiredTime: '',\n    unlimitedQuota: true,\n    modelLimitsEnabled: false,\n    modelLimits: [],\n    allowIps: '',\n    group: 'default'\n  })\n\n  const [isLoading, setIsLoading] = useState(false)\n  const [isSubmitting, setIsSubmitting] = useState(false)\n  const [availableModels, setAvailableModels] = useState<string[]>([])\n  const [groups, setGroups] = useState<Record<string, GroupInfo>>({})\n  const [errors, setErrors] = useState<Record<string, string>>({})\n\n  // 获取当前选中的账号\n  const currentAccount = availableAccounts.find(acc => acc.id === formData.accountId)\n  \n  // 判断是否为编辑模式\n  const isEditMode = !!editingToken\n\n  // 初始化表单数据\n  useEffect(() => {\n    if (isOpen) {\n      if (isEditMode && editingToken) {\n        // 编辑模式：从 editingToken 填充表单数据\n        const matchingAccount = availableAccounts.find(acc => acc.name === editingToken.accountName)\n        const accountId = matchingAccount?.id || (availableAccounts.length > 0 ? availableAccounts[0].id : '')\n        \n        setFormData({\n          accountId,\n          name: editingToken.name,\n          quota: editingToken.unlimited_quota ? '' : (editingToken.remain_quota / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR).toString(),\n          expiredTime: editingToken.expired_time === -1 ? '' : new Date(editingToken.expired_time * 1000).toISOString().slice(0, 16),\n          unlimitedQuota: editingToken.unlimited_quota,\n          modelLimitsEnabled: editingToken.model_limits_enabled || false,\n          modelLimits: editingToken.model_limits ? editingToken.model_limits.split(',') : [],\n          allowIps: editingToken.allow_ips || '',\n          group: editingToken.group || 'default'\n        })\n      } else {\n        // 创建模式：使用默认值\n        const defaultAccountId = preSelectedAccountId || (availableAccounts.length > 0 ? availableAccounts[0].id : '')\n        setFormData({\n          accountId: defaultAccountId,\n          name: '',\n          quota: '',\n          expiredTime: '',\n          unlimitedQuota: true,\n          modelLimitsEnabled: false,\n          modelLimits: [],\n          allowIps: '',\n          group: 'default'\n        })\n      }\n    }\n  }, [isOpen, preSelectedAccountId, availableAccounts, isEditMode, editingToken])\n\n  // 加载数据\n  useEffect(() => {\n    if (isOpen && currentAccount) {\n      loadInitialData()\n    }\n  }, [isOpen, currentAccount])\n\n  const loadInitialData = async () => {\n    if (!currentAccount) return\n    \n    setIsLoading(true)\n    try {\n      const [models, groupsData] = await Promise.all([\n        fetchAvailableModels(currentAccount.baseUrl, currentAccount.userId, currentAccount.token),\n        fetchUserGroups(currentAccount.baseUrl, currentAccount.userId, currentAccount.token)\n      ])\n      \n      setAvailableModels(models)\n      setGroups(groupsData)\n      \n      // 设置默认分组\n      if (groupsData.default) {\n        setFormData(prev => ({ ...prev, group: 'default' }))\n      } else {\n        const firstGroup = Object.keys(groupsData)[0]\n        if (firstGroup) {\n          setFormData(prev => ({ ...prev, group: firstGroup }))\n        }\n      }\n    } catch (error) {\n      console.error('加载初始数据失败:', error)\n      toast.error('加载数据失败，请稍后重试')\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  // 验证表单\n  const validateForm = (): boolean => {\n    const newErrors: Record<string, string> = {}\n\n    if (!formData.accountId) {\n      newErrors.accountId = '请选择账号'\n    }\n\n    if (!formData.name.trim()) {\n      newErrors.name = '密钥名称不能为空'\n    }\n\n    if (!formData.unlimitedQuota) {\n      const quota = parseFloat(formData.quota)\n      if (isNaN(quota) || quota <= 0) {\n        newErrors.quota = '请输入有效的额度金额'\n      }\n    }\n\n    if (formData.expiredTime) {\n      const expiredDate = new Date(formData.expiredTime)\n      if (expiredDate <= new Date()) {\n        newErrors.expiredTime = '过期时间必须大于当前时间'\n      }\n    }\n\n    if (formData.allowIps && !isValidIpList(formData.allowIps)) {\n      newErrors.allowIps = '请输入有效的IP地址，多个IP用逗号分隔'\n    }\n\n    setErrors(newErrors)\n    return Object.keys(newErrors).length === 0\n  }\n\n  // 验证IP地址列表\n  const isValidIpList = (ips: string): boolean => {\n    const ipList = ips.split(',').map(ip => ip.trim())\n    const ipRegex = /^(\\d{1,3}\\.){3}\\d{1,3}$/\n    \n    return ipList.every(ip => {\n      if (!ip) return false\n      if (ip === '*') return true // 允许通配符\n      return ipRegex.test(ip) && ip.split('.').every(part => {\n        const num = parseInt(part)\n        return num >= 0 && num <= 255\n      })\n    })\n  }\n\n  // 处理表单提交\n  const handleSubmit = async () => {\n    if (!currentAccount || !validateForm()) return\n\n    setIsSubmitting(true)\n    try {\n      // 准备请求数据\n      const tokenData: CreateTokenRequest = {\n        name: formData.name.trim(),\n        remain_quota: formData.unlimitedQuota ? -1 : Math.floor(parseFloat(formData.quota) * UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR),\n        expired_time: formData.expiredTime ? Math.floor(new Date(formData.expiredTime).getTime() / 1000) : -1,\n        unlimited_quota: formData.unlimitedQuota,\n        model_limits_enabled: formData.modelLimitsEnabled,\n        model_limits: formData.modelLimits.join(','),\n        allow_ips: formData.allowIps.trim() || '',\n        group: formData.group\n      }\n\n      if (isEditMode && editingToken) {\n        // 编辑模式\n        await updateApiToken(currentAccount.baseUrl, currentAccount.userId, currentAccount.token, editingToken.id, tokenData)\n        toast.success('密钥更新成功')\n      } else {\n        // 创建模式\n        await createApiToken(currentAccount.baseUrl, currentAccount.userId, currentAccount.token, tokenData)\n        toast.success('密钥创建成功')\n      }\n      \n      handleClose()\n    } catch (error) {\n      console.error(`${isEditMode ? '更新' : '创建'}密钥失败:`, error)\n      toast.error(`${isEditMode ? '更新' : '创建'}密钥失败，请稍后重试`)\n    } finally {\n      setIsSubmitting(false)\n    }\n  }\n\n  // 关闭对话框\n  const handleClose = () => {\n    setFormData({\n      accountId: '',\n      name: '',\n      quota: '',\n      expiredTime: '',\n      unlimitedQuota: true,\n      modelLimitsEnabled: false,\n      modelLimits: [],\n      allowIps: '',\n      group: 'default'\n    })\n    setErrors({})\n    setAvailableModels([])\n    setGroups({})\n    onClose()\n  }\n\n  return (\n    <Transition appear show={isOpen} as={Fragment}>\n      <Dialog as=\"div\" className=\"relative z-50\" onClose={handleClose}>\n        <Transition.Child\n          as={Fragment}\n          enter=\"ease-out duration-300\"\n          enterFrom=\"opacity-0\"\n          enterTo=\"opacity-100\"\n          leave=\"ease-in duration-200\"\n          leaveFrom=\"opacity-100\"\n          leaveTo=\"opacity-0\"\n        >\n          <div className=\"fixed inset-0 bg-black bg-opacity-25\" />\n        </Transition.Child>\n\n        <div className=\"fixed inset-0 overflow-y-auto\">\n          <div className=\"flex min-h-full items-center justify-center p-4\">\n            <Transition.Child\n              as={Fragment}\n              enter=\"ease-out duration-300\"\n              enterFrom=\"opacity-0 scale-95\"\n              enterTo=\"opacity-100 scale-100\"\n              leave=\"ease-in duration-200\"\n              leaveFrom=\"opacity-100 scale-100\"\n              leaveTo=\"opacity-0 scale-95\"\n            >\n              <Dialog.Panel className=\"w-full max-w-2xl transform overflow-hidden rounded-lg bg-white p-6 shadow-xl transition-all\">\n                {/* 标题栏 */}\n                <div className=\"flex items-center justify-between mb-6\">\n                  <div className=\"flex items-center space-x-2\">\n                    <KeyIcon className=\"w-6 h-6 text-blue-600\" />\n                    <Dialog.Title className=\"text-lg font-semibold text-gray-900\">\n                      {isEditMode ? '编辑API密钥' : '添加API密钥'}\n                    </Dialog.Title>\n                  </div>\n                  <button\n                    onClick={handleClose}\n                    className=\"p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors\"\n                  >\n                    <XMarkIcon className=\"w-5 h-5\" />\n                  </button>\n                </div>\n\n                {isLoading ? (\n                  <div className=\"space-y-4\">\n                    <div className=\"animate-pulse\">\n                      <div className=\"h-4 bg-gray-200 rounded w-1/4 mb-4\"></div>\n                      <div className=\"space-y-3\">\n                        <div className=\"h-10 bg-gray-200 rounded\"></div>\n                        <div className=\"h-10 bg-gray-200 rounded\"></div>\n                        <div className=\"h-10 bg-gray-200 rounded\"></div>\n                      </div>\n                    </div>\n                  </div>\n                ) : (\n                  <div className=\"space-y-6\">\n                    {/* 基本信息 */}\n                    <div className=\"space-y-4\">\n                      <h3 className=\"text-sm font-medium text-gray-900\">基本信息</h3>\n                      \n                      {/* 账号选择 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n                          选择账号 <span className=\"text-red-500\">*</span>\n                        </label>\n                        <select\n                          value={formData.accountId}\n                          onChange={(e) => setFormData(prev => ({ ...prev, accountId: e.target.value }))}\n                          disabled={isEditMode} // 编辑模式下禁用账号选择\n                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${\n                            errors.accountId ? 'border-red-300' : 'border-gray-300'\n                          } ${isEditMode ? 'bg-gray-100 cursor-not-allowed' : ''}`}\n                        >\n                          <option value=\"\">请选择账号</option>\n                          {availableAccounts.map(account => (\n                            <option key={account.id} value={account.id}>\n                              {account.name}\n                            </option>\n                          ))}\n                        </select>\n                        {errors.accountId && (\n                          <p className=\"mt-1 text-xs text-red-600\">{errors.accountId}</p>\n                        )}\n                        {isEditMode && (\n                          <p className=\"mt-1 text-xs text-gray-500\">编辑模式下无法更改账号</p>\n                        )}\n                      </div>\n                      \n                      {/* 密钥名称 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n                          密钥名称 <span className=\"text-red-500\">*</span>\n                        </label>\n                        <input\n                          type=\"text\"\n                          value={formData.name}\n                          onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}\n                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${\n                            errors.name ? 'border-red-300' : 'border-gray-300'\n                          }`}\n                          placeholder=\"请输入密钥名称\"\n                        />\n                        {errors.name && (\n                          <p className=\"mt-1 text-xs text-red-600\">{errors.name}</p>\n                        )}\n                      </div>\n\n                      {/* 额度设置 */}\n                      <div className=\"space-y-3\">\n                        <div className=\"flex items-center justify-between\">\n                          <label className=\"text-sm font-medium text-gray-700\">额度设置</label>\n                          <div className=\"flex items-center space-x-2\">\n                            <span className=\"text-sm text-gray-500\">无限额度</span>\n                            <Switch\n                              checked={formData.unlimitedQuota}\n                              onChange={(checked) => setFormData(prev => ({ ...prev, unlimitedQuota: checked }))}\n                              className={`${\n                                formData.unlimitedQuota ? 'bg-blue-600' : 'bg-gray-200'\n                              } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}\n                            >\n                              <span\n                                className={`${\n                                  formData.unlimitedQuota ? 'translate-x-6' : 'translate-x-1'\n                                } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}\n                              />\n                            </Switch>\n                          </div>\n                        </div>\n                        \n                        {!formData.unlimitedQuota && (\n                          <div>\n                            <input\n                              type=\"number\"\n                              step=\"0.01\"\n                              min=\"0\"\n                              value={formData.quota}\n                              onChange={(e) => setFormData(prev => ({ ...prev, quota: e.target.value }))}\n                              className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${\n                                errors.quota ? 'border-red-300' : 'border-gray-300'\n                              }`}\n                              placeholder=\"请输入额度金额（美元）\"\n                            />\n                            {errors.quota && (\n                              <p className=\"mt-1 text-xs text-red-600\">{errors.quota}</p>\n                            )}\n                            <p className=\"mt-1 text-xs text-gray-500\">\n                              1美元 = {UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR.toLocaleString()} 配额点数\n                            </p>\n                          </div>\n                        )}\n                      </div>\n\n                      {/* 过期时间 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n                          过期时间\n                        </label>\n                        <input\n                          type=\"datetime-local\"\n                          value={formData.expiredTime}\n                          onChange={(e) => setFormData(prev => ({ ...prev, expiredTime: e.target.value }))}\n                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${\n                            errors.expiredTime ? 'border-red-300' : 'border-gray-300'\n                          }`}\n                        />\n                        {errors.expiredTime && (\n                          <p className=\"mt-1 text-xs text-red-600\">{errors.expiredTime}</p>\n                        )}\n                        <p className=\"mt-1 text-xs text-gray-500\">留空表示永不过期</p>\n                      </div>\n                    </div>\n\n                    {/* 高级设置 */}\n                    <div className=\"space-y-4\">\n                      <h3 className=\"text-sm font-medium text-gray-900\">高级设置</h3>\n                      \n                      {/* 分组选择 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n                          分组\n                        </label>\n                        <select\n                          value={formData.group}\n                          onChange={(e) => setFormData(prev => ({ ...prev, group: e.target.value }))}\n                          className=\"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n                        >\n                          {Object.entries(groups).map(([key, group]) => (\n                            <option key={key} value={key}>\n                              {group.desc} (倍率: {group.ratio})\n                            </option>\n                          ))}\n                        </select>\n                      </div>\n\n                      {/* 模型限制 */}\n                      <div className=\"space-y-3\">\n                        <div className=\"flex items-center justify-between\">\n                          <label className=\"text-sm font-medium text-gray-700\">模型限制</label>\n                          <Switch\n                            checked={formData.modelLimitsEnabled}\n                            onChange={(enabled) => setFormData(prev => ({ \n                              ...prev, \n                              modelLimitsEnabled: enabled,\n                              modelLimits: enabled ? prev.modelLimits : []\n                            }))}\n                            className={`${\n                              formData.modelLimitsEnabled ? 'bg-blue-600' : 'bg-gray-200'\n                            } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}\n                          >\n                            <span\n                              className={`${\n                                formData.modelLimitsEnabled ? 'translate-x-6' : 'translate-x-1'\n                              } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}\n                            />\n                          </Switch>\n                        </div>\n\n                        {formData.modelLimitsEnabled && (\n                          <div>\n                            <select\n                              multiple\n                              value={formData.modelLimits}\n                              onChange={(e) => {\n                                const values = Array.from(e.target.selectedOptions, option => option.value)\n                                setFormData(prev => ({ ...prev, modelLimits: values }))\n                              }}\n                              className=\"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 h-32\"\n                            >\n                              {availableModels.map((model) => (\n                                <option key={model} value={model}>\n                                  {model}\n                                </option>\n                              ))}\n                            </select>\n                            <p className=\"mt-1 text-xs text-gray-500\">\n                              按住 Ctrl/Cmd 键可多选模型，已选择 {formData.modelLimits.length} 个模型\n                            </p>\n                          </div>\n                        )}\n                      </div>\n\n                      {/* IP 限制 */}\n                      <div>\n                        <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n                          IP限制\n                        </label>\n                        <input\n                          type=\"text\"\n                          value={formData.allowIps}\n                          onChange={(e) => setFormData(prev => ({ ...prev, allowIps: e.target.value }))}\n                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${\n                            errors.allowIps ? 'border-red-300' : 'border-gray-300'\n                          }`}\n                          placeholder=\"留空表示不限制，多个IP用逗号分隔\"\n                        />\n                        {errors.allowIps && (\n                          <p className=\"mt-1 text-xs text-red-600\">{errors.allowIps}</p>\n                        )}\n                        <p className=\"mt-1 text-xs text-gray-500\">\n                          例如: 192.168.1.1,10.0.0.1 或使用 * 表示不限制\n                        </p>\n                      </div>\n                    </div>\n\n                    {/* 警告提示 */}\n                    <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-3\">\n                      <div className=\"flex items-start space-x-2\">\n                        <ExclamationTriangleIcon className=\"w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0\" />\n                        <div className=\"text-sm text-yellow-800\">\n                          <p className=\"font-medium mb-1\">注意事项</p>\n                          <ul className=\"text-xs space-y-1\">\n                            <li>• 请妥善保管API密钥，避免泄露</li>\n                          </ul>\n                        </div>\n                      </div>\n                    </div>\n\n                    {/* 操作按钮 */}\n                    <div className=\"flex justify-end space-x-3 pt-4\">\n                      <button\n                        onClick={handleClose}\n                        disabled={isSubmitting}\n                        className=\"px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50\"\n                      >\n                        取消\n                      </button>\n                      <button\n                        onClick={handleSubmit}\n                        disabled={isSubmitting || !currentAccount}\n                        className=\"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center space-x-2\"\n                      >\n                        {isSubmitting && (\n                          <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin\"></div>\n                        )}\n                        <span>{isSubmitting ? (isEditMode ? '更新中...' : '创建中...') : (isEditMode ? '更新密钥' : '创建密钥')}</span>\n                      </button>\n                    </div>\n                  </div>\n                )}\n              </Dialog.Panel>\n            </Transition.Child>\n          </div>\n        </div>\n      </Dialog>\n    </Transition>\n  )\n}"
  },
  {
    "path": "components/AutoDetectErrorAlert.tsx",
    "content": "import { Fragment } from 'react'\nimport { ExclamationTriangleIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'\nimport type { AutoDetectError, AutoDetectErrorProps } from '../utils/autoDetectUtils'\nimport { openLoginTab } from '../utils/autoDetectUtils'\n\nexport default function AutoDetectErrorAlert({ \n  error, \n  siteUrl, \n  onHelpClick, \n  onActionClick \n}: AutoDetectErrorProps) {\n  const handleActionClick = () => {\n    if (onActionClick) {\n      onActionClick()\n    } else if (error.type === 'unauthorized' && siteUrl) {\n      // 默认行为：打开登录页面\n      openLoginTab(siteUrl)\n    }\n  }\n\n  const handleHelpClick = () => {\n    if (onHelpClick) {\n      onHelpClick()\n    } else if (error.helpDocUrl) {\n      // 默认行为：打开帮助文档\n      chrome.tabs.create({ url: error.helpDocUrl })\n    }\n  }\n\n  return (\n    <div className=\"bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4\">\n      <div className=\"flex\">\n        <div className=\"flex-shrink-0\">\n          <ExclamationTriangleIcon className=\"h-4 w-4 text-amber-400\" />\n        </div>\n        <div className=\"ml-2 flex-1\">\n          <p className=\"text-xs text-amber-700\">{error.message}</p>\n          \n          {/* 操作按钮区域 */}\n          {(error.actionText || error.helpDocUrl) && (\n            <div className=\"mt-2 flex space-x-2\">\n              {/* 主要操作按钮 */}\n              {error.actionText && (\n                <button\n                  type=\"button\"\n                  onClick={handleActionClick}\n                  className=\"inline-flex items-center px-2 py-1 text-xs font-medium text-amber-800 bg-amber-100 border border-amber-300 rounded hover:bg-amber-200 transition-colors\"\n                >\n                  {error.actionText}\n                </button>\n              )}\n              \n              {/* 帮助文档按钮 */}\n              {error.helpDocUrl && (\n                <button\n                  type=\"button\"\n                  onClick={handleHelpClick}\n                  className=\"inline-flex items-center px-2 py-1 text-xs font-medium text-amber-600 hover:text-amber-800 transition-colors\"\n                >\n                  <QuestionMarkCircleIcon className=\"w-3 h-3 mr-1\" />\n                  帮助文档\n                </button>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "components/BalanceSection.tsx",
    "content": "import { Tab, TabGroup, TabList, TabPanel, TabPanels } from \"@headlessui/react\"\nimport { ArrowUpIcon, ArrowDownIcon } from \"@heroicons/react/24/outline\"\nimport CountUp from \"react-countup\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport { getCurrencySymbol, formatTokenCount } from \"../utils/formatters\"\nimport { useTimeFormatter } from \"../hooks/useTimeFormatter\"\nimport Tooltip from \"./Tooltip\"\n\ninterface BalanceSectionProps {\n  // 金额数据\n  totalConsumption: { USD: number; CNY: number }\n  totalBalance: { USD: number; CNY: number }\n  todayTokens: { upload: number; download: number }\n  \n  // 状态\n  currencyType: 'USD' | 'CNY'\n  activeTab: 'consumption' | 'balance'\n  isInitialLoad: boolean\n  lastUpdateTime: Date\n  \n  // 动画相关\n  prevTotalConsumption: { USD: number; CNY: number }\n  \n  // 事件处理\n  onCurrencyToggle: () => void\n  onTabChange: (index: number) => void\n}\n\nexport default function BalanceSection({\n  totalConsumption,\n  totalBalance,\n  todayTokens,\n  currencyType,\n  activeTab,\n  isInitialLoad,\n  lastUpdateTime,\n  prevTotalConsumption,\n  onCurrencyToggle,\n  onTabChange\n}: BalanceSectionProps) {\n  const { formatRelativeTime, formatFullTime } = useTimeFormatter()\n  \n  return (\n    <div className=\"px-6 py-6 bg-gradient-to-br from-blue-50/50 to-indigo-50/30\">\n      <div className=\"space-y-3\">\n        {/* 金额标签页 */}\n        <div>\n          <TabGroup selectedIndex={activeTab === 'consumption' ? 0 : 1} onChange={onTabChange}>\n            <div className=\"flex justify-start mb-3\">\n              <TabList className=\"flex space-x-1 bg-gray-100 rounded-lg p-1\">\n                <Tab className={({ selected }) => \n                  `px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${\n                    selected \n                      ? 'bg-white text-gray-900 shadow-sm' \n                      : 'text-gray-500 hover:text-gray-700'\n                  }`\n                }>\n                  今日消耗\n                </Tab>\n                <Tab className={({ selected }) => \n                  `px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${\n                    selected \n                      ? 'bg-white text-gray-900 shadow-sm' \n                      : 'text-gray-500 hover:text-gray-700'\n                  }`\n                }>\n                  总余额\n                </Tab>\n              </TabList>\n            </div>\n            \n            <TabPanels>\n              <TabPanel>\n                {/* 今日消耗面板 */}\n                <div className=\"flex items-center space-x-1\">\n                  <button\n                    onClick={onCurrencyToggle}\n                    className=\"text-5xl font-bold text-gray-900 tracking-tight hover:text-blue-600 transition-colors cursor-pointer\"\n                    title={`点击切换到 ${currencyType === 'USD' ? '人民币' : '美元'}`}\n                  >\n                    {totalConsumption[currencyType] > 0 ? '-' : ''}{getCurrencySymbol(currencyType)}\n                    <CountUp\n                      start={isInitialLoad ? 0 : prevTotalConsumption[currencyType]}\n                      end={totalConsumption[currencyType]}\n                      duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.INITIAL_DURATION : UI_CONSTANTS.ANIMATION.UPDATE_DURATION}\n                      decimals={2}\n                      preserveValue\n                    />\n                  </button>\n                </div>\n              </TabPanel>\n              \n              <TabPanel>\n                {/* 总余额面板 */}\n                <div className=\"flex items-center space-x-1\">\n                  <button\n                    onClick={onCurrencyToggle}\n                    className=\"text-5xl font-bold text-gray-900 tracking-tight hover:text-blue-600 transition-colors cursor-pointer\"\n                    title={`点击切换到 ${currencyType === 'USD' ? '人民币' : '美元'}`}\n                  >\n                    {getCurrencySymbol(currencyType)}\n                    <CountUp\n                      start={isInitialLoad ? 0 : 0}\n                      end={totalBalance[currencyType]}\n                      duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.INITIAL_DURATION : UI_CONSTANTS.ANIMATION.UPDATE_DURATION}\n                      decimals={2}\n                      preserveValue\n                    />\n                  </button>\n                </div>\n              </TabPanel>\n            </TabPanels>\n          </TabGroup>\n        </div>\n        \n        {/* Token 统计信息 */}\n        <div>\n          <Tooltip\n            content={\n              <div>\n                <div>提示: {todayTokens.upload.toLocaleString()} tokens</div>\n                <div>补全: {todayTokens.download.toLocaleString()} tokens</div>\n              </div>\n            }\n          >\n            <div className=\"flex items-center space-x-3 cursor-help\">\n              <div className=\"flex items-center space-x-1\">\n                <ArrowUpIcon className=\"w-4 h-4 text-green-500\" />\n                <span className=\"font-medium text-gray-500\">\n                  {formatTokenCount(todayTokens.upload)}\n                </span>\n              </div>\n              <div className=\"flex items-center space-x-1\">\n                <ArrowDownIcon className=\"w-4 h-4 text-blue-500\" />\n                <span className=\"font-medium text-gray-500\">\n                  {formatTokenCount(todayTokens.download)}\n                </span>\n              </div>\n            </div>\n          </Tooltip>\n        </div>\n      </div>\n      \n      {/* 最后更新时间 */}\n      <div className=\"mt-4 pt-3 border-t border-gray-100\">\n        <div className=\"ml-2\">\n          <Tooltip content={formatFullTime(lastUpdateTime)}>\n            <p className=\"text-xs text-gray-400 cursor-help\">\n              更新于 {formatRelativeTime(lastUpdateTime)}\n            </p>\n          </Tooltip>\n        </div>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "components/CopyKeyDialog.tsx",
    "content": "import { Fragment, useState, useEffect } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from \"@headlessui/react\"\nimport { \n  XMarkIcon, \n  KeyIcon, \n  DocumentDuplicateIcon, \n  ExclamationTriangleIcon,\n  CheckIcon,\n  ClockIcon,\n  UserGroupIcon,\n  ChevronDownIcon,\n  ChevronRightIcon\n} from \"@heroicons/react/24/outline\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport { fetchAccountTokens, type ApiToken } from \"../services/apiService\"\nimport type { DisplaySiteData } from \"../types\"\n\ninterface CopyKeyDialogProps {\n  isOpen: boolean\n  onClose: () => void\n  account: DisplaySiteData | null\n}\n\nexport default function CopyKeyDialog({ isOpen, onClose, account }: CopyKeyDialogProps) {\n  const [tokens, setTokens] = useState<ApiToken[]>([])\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n  const [copiedKey, setCopiedKey] = useState<string | null>(null)\n  const [expandedTokens, setExpandedTokens] = useState<Set<number>>(new Set())\n\n  // 获取密钥列表\n  const fetchTokens = async () => {\n    if (!account) return\n\n    setIsLoading(true)\n    setError(null)\n    \n    try {\n      // 使用 DisplaySiteData 中的 userId 字段\n      const tokensResponse = await fetchAccountTokens(account.baseUrl, account.userId, account.token)\n      \n      // 确保返回的是数组\n      if (Array.isArray(tokensResponse)) {\n        setTokens(tokensResponse)\n      } else {\n        console.warn('Token response is not an array:', tokensResponse)\n        setTokens([])\n      }\n    } catch (error) {\n      console.error('获取密钥列表失败:', error)\n      const errorMessage = error instanceof Error ? error.message : '未知错误'\n      setError(`获取密钥列表失败: ${errorMessage}`)\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  // 当对话框打开时获取密钥列表\n  useEffect(() => {\n    if (isOpen && account) {\n      fetchTokens()\n    } else {\n      // 关闭时重置状态\n      setTokens([])\n      setError(null)\n      setCopiedKey(null)\n      setExpandedTokens(new Set())\n    }\n  }, [isOpen, account])\n\n  // 复制密钥到剪贴板\n  const copyKey = async (key: string) => {\n    try {\n      // 检查key是否以\"sk-\"开头，如果不是则添加前缀\n      const textToCopy = key.startsWith('sk-') ? key : 'sk-' + key;\n      await navigator.clipboard.writeText(textToCopy);\n      setCopiedKey(key);\n      toast.success('密钥已复制到剪贴板');\n      \n      // 2秒后清除复制状态\n      setTimeout(() => {\n        setCopiedKey(null);\n      }, 2000);\n    } catch (error) {\n      console.error('复制失败:', error);\n      toast.error('复制失败，请手动复制');\n    }\n};\n\n\n  // 切换密钥展开/折叠状态\n  const toggleTokenExpansion = (tokenId: number) => {\n    setExpandedTokens(prev => {\n      const newSet = new Set(prev)\n      if (newSet.has(tokenId)) {\n        newSet.delete(tokenId)\n      } else {\n        newSet.add(tokenId)\n      }\n      return newSet\n    })\n  }\n\n  // 格式化额度显示\n  const formatQuota = (token: ApiToken) => {\n    if (token.unlimited_quota || token.remain_quota < 0) {\n      return '无限额度'\n    }\n    \n    // 使用CONVERSION_FACTOR转换真实额度\n    const realQuota = token.remain_quota / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR\n    return `$${realQuota.toFixed(2)}`\n  }\n\n  // 格式化已用额度\n  const formatUsedQuota = (token: ApiToken) => {\n    const realUsedQuota = token.used_quota / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR\n    return `$${realUsedQuota.toFixed(2)}`\n  }\n\n  // 格式化时间\n  const formatTime = (timestamp: number) => {\n    if (timestamp <= 0) return '永不过期'\n    return new Date(timestamp * 1000).toLocaleDateString('zh-CN')\n  }\n\n  // 获取组别徽章样式\n  const getGroupBadgeStyle = (group: string) => {\n    // 处理可能为空或未定义的 group\n    const groupName = group || 'default'\n    \n    // 根据组别名称生成不同的颜色主题\n    const hash = groupName.split('').reduce((a, b) => {\n      a = ((a << 5) - a) + b.charCodeAt(0)\n      return a & a\n    }, 0)\n    \n    const colors = [\n      'bg-blue-100 text-blue-800 border-blue-200',\n      'bg-green-100 text-green-800 border-green-200', \n      'bg-purple-100 text-purple-800 border-purple-200',\n      'bg-orange-100 text-orange-800 border-orange-200',\n      'bg-pink-100 text-pink-800 border-pink-200',\n      'bg-indigo-100 text-indigo-800 border-indigo-200',\n      'bg-teal-100 text-teal-800 border-teal-200',\n      'bg-yellow-100 text-yellow-800 border-yellow-200'\n    ]\n    \n    return colors[Math.abs(hash) % colors.length]\n  }\n\n  // 获取状态徽章样式\n  const getStatusBadgeStyle = (status: number) => {\n    return status === 1 \n      ? 'bg-green-100 text-green-800 border-green-200'\n      : 'bg-red-100 text-red-800 border-red-200'\n  }\n\n  return (\n    <Transition show={isOpen} as={Fragment}>\n      <Dialog\n        onClose={onClose}\n        className=\"relative z-50\"\n      >\n        {/* 背景遮罩动画 */}\n        <TransitionChild\n          as={Fragment}\n          enter=\"ease-out duration-300\"\n          enterFrom=\"opacity-0\"\n          enterTo=\"opacity-100\"\n          leave=\"ease-in duration-200\"\n          leaveFrom=\"opacity-100\"\n          leaveTo=\"opacity-0\"\n        >\n          <div className=\"fixed inset-0 bg-black/30 backdrop-blur-sm\" aria-hidden=\"true\" />\n        </TransitionChild>\n        \n        {/* 居中容器 */}\n        <div className=\"fixed inset-0 flex items-center justify-center p-4\">\n          {/* 弹窗面板动画 */}\n          <TransitionChild\n            as={Fragment}\n            enter=\"ease-out duration-300\"\n            enterFrom=\"opacity-0 scale-95 translate-y-4\"\n            enterTo=\"opacity-100 scale-100 translate-y-0\"\n            leave=\"ease-in duration-200\"\n            leaveFrom=\"opacity-100 scale-100 translate-y-0\"\n            leaveTo=\"opacity-0 scale-95 translate-y-4\"\n          >\n            <DialogPanel className=\"w-full max-w-md bg-white rounded-lg shadow-xl transform transition-all max-h-[85vh] overflow-hidden flex flex-col\">\n              {/* 头部 */}\n              <div className=\"flex items-center justify-between p-4 border-b border-gray-100\">\n                <div className=\"flex items-center space-x-3\">\n                  <div className=\"w-8 h-8 bg-gradient-to-r from-purple-500 to-indigo-600 rounded-lg flex items-center justify-center\">\n                    <KeyIcon className=\"w-4 h-4 text-white\" />\n                  </div>\n                  <div>\n                    <DialogTitle className=\"text-lg font-semibold text-gray-900\">\n                      密钥列表\n                    </DialogTitle>\n                    <p className=\"text-xs text-gray-500 mt-0.5\">\n                      {account?.name}\n                    </p>\n                  </div>\n                </div>\n                <button\n                  onClick={onClose}\n                  className=\"p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors\"\n                >\n                  <XMarkIcon className=\"w-4 h-4\" />\n                </button>\n              </div>\n\n              {/* 内容区域 */}\n              <div className=\"flex-1 overflow-y-auto p-4\">\n                {isLoading ? (\n                  <div className=\"flex flex-col items-center justify-center py-8\">\n                    <div className=\"w-8 h-8 border-2 border-purple-300 border-t-purple-600 rounded-full animate-spin mb-4\" />\n                    <p className=\"text-sm text-gray-500\">正在获密钥列表...</p>\n                  </div>\n                ) : error ? (\n                  <div className=\"bg-red-50 border border-red-200 rounded-lg p-4\">\n                    <div className=\"flex items-start\">\n                      <ExclamationTriangleIcon className=\"w-5 h-5 text-red-400 flex-shrink-0 mt-0.5\" />\n                      <div className=\"ml-3\">\n                        <h3 className=\"text-sm font-medium text-red-800\">获取失败</h3>\n                        <p className=\"text-sm text-red-700 mt-1\">{error}</p>\n                        <button\n                          onClick={fetchTokens}\n                          className=\"mt-3 px-3 py-1.5 bg-red-100 text-red-800 text-xs rounded-lg hover:bg-red-200 transition-colors\"\n                        >\n                          重试\n                        </button>\n                      </div>\n                    </div>\n                  </div>\n                ) : !Array.isArray(tokens) || tokens.length === 0 ? (\n                  <div className=\"text-center py-8\">\n                    <KeyIcon className=\"w-12 h-12 text-gray-300 mx-auto mb-4\" />\n                    <p className=\"text-gray-500 text-sm\">暂无密钥数据</p>\n                  </div>\n                ) : (\n                  <div className=\"space-y-3\">\n                    {Array.isArray(tokens) && tokens.map((token) => {\n                      const isExpanded = expandedTokens.has(token.id)\n                      \n                      return (\n                        <div\n                          key={token.id}\n                          className=\"bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-sm transition-all duration-200\"\n                        >\n                          {/* 头部：名称、组别徽章和展开/折叠按钮 */}\n                          <div \n                            className=\"flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 transition-colors\"\n                            onClick={() => toggleTokenExpansion(token.id)}\n                          >\n                            <div className=\"flex-1 min-w-0 space-y-1.5\">\n                              <h4 className=\"font-medium text-gray-900 text-sm truncate\">\n                                {token.name}\n                              </h4>\n                              <div className=\"flex items-center space-x-1.5\">\n                                <UserGroupIcon className=\"w-3 h-3 text-gray-400\" />\n                                <span \n                                  className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${getGroupBadgeStyle(token.group || '')}`}\n                                >\n                                  {token.group || '默认组'}\n                                </span>\n                              </div>\n                            </div>\n                            \n                            <div className=\"flex items-center space-x-2 ml-3\">\n                              {/* 状态徽章 */}\n                              <span \n                                className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium border ${getStatusBadgeStyle(token.status)}`}\n                              >\n                                {token.status === 1 ? '启用' : '禁用'}\n                              </span>\n                              \n                              {/* 展开/折叠图标 */}\n                              {isExpanded ? (\n                                <ChevronDownIcon className=\"w-4 h-4 text-gray-400\" />\n                              ) : (\n                                <ChevronRightIcon className=\"w-4 h-4 text-gray-400\" />\n                              )}\n                            </div>\n                          </div>\n\n                          {/* 可展开的详细信息区域 */}\n                          {isExpanded && (\n                            <div className=\"px-3 pb-3 border-t border-gray-100 bg-gray-50/30\">\n                              {/* 过期时间 */}\n                              <div className=\"flex items-center space-x-1 text-xs text-gray-500 mb-3 pt-3\">\n                                <ClockIcon className=\"w-3 h-3\" />\n                                <span>过期时间: {formatTime(token.expired_time)}</span>\n                              </div>\n\n                              {/* 额度信息网格 */}\n                              <div className=\"grid grid-cols-2 gap-2 mb-3\">\n                                <div className=\"bg-white rounded p-2 border border-gray-100\">\n                                  <div className=\"text-xs text-gray-500 mb-0.5\">已用额度</div>\n                                  <div className=\"text-sm font-semibold text-gray-900\">\n                                    {formatUsedQuota(token)}\n                                  </div>\n                                </div>\n                                <div className=\"bg-white rounded p-2 border border-gray-100\">\n                                  <div className=\"text-xs text-gray-500 mb-0.5\">剩余额度</div>\n                                  <div className={`text-sm font-semibold ${\n                                    token.unlimited_quota || token.remain_quota < 0 \n                                      ? 'text-green-600' \n                                      : token.remain_quota < 1000000 \n                                        ? 'text-orange-600' \n                                        : 'text-gray-900'\n                                  }`}>\n                                    {formatQuota(token)}\n                                  </div>\n                                </div>\n                              </div>\n\n                              {/* 密钥预览 */}\n                              <div className=\"bg-white rounded p-2 border border-gray-100\">\n                                <div className=\"flex items-center justify-between mb-1\">\n                                  <span className=\"text-xs font-medium text-gray-500 uppercase tracking-wide\">API 密钥</span>\n                                  <button\n                                    onClick={(e) => {\n                                      e.stopPropagation()\n                                      copyKey(token.key)\n                                    }}\n                                    className=\"flex items-center space-x-1 px-2 py-1 bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-medium rounded hover:from-purple-600 hover:to-indigo-700 transition-all duration-200\"\n                                  >\n                                    {copiedKey === token.key ? (\n                                      <>\n                                        <CheckIcon className=\"w-3 h-3\" />\n                                        <span>已复制</span>\n                                      </>\n                                    ) : (\n                                      <>\n                                        <DocumentDuplicateIcon className=\"w-3 h-3\" />\n                                        <span>复制</span>\n                                      </>\n                                    )}\n                                  </button>\n                                </div>\n                                <div className=\"font-mono text-xs text-gray-700 bg-gray-50 px-2 py-1 rounded border border-gray-200 break-all\">\n                                  <span className=\"text-gray-900\">{token.key.substring(0, 16)}</span>\n                                  <span className=\"text-gray-400\">{'•'.repeat(6)}</span>\n                                  <span className=\"text-gray-900\">{token.key.substring(token.key.length - 6)}</span>\n                                </div>\n                              </div>\n                            </div>\n                          )}\n                        </div>\n                      )\n                    })}\n                  </div>\n                )}\n              </div>\n\n              {/* 底部操作区 */}\n              <div className=\"px-4 py-3 border-t border-gray-100 bg-gray-50/50\">\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"flex items-center space-x-2\">\n                    {tokens.length > 0 && (\n                      <div className=\"flex items-center space-x-1.5 text-xs text-gray-500\">\n                        <KeyIcon className=\"w-3 h-3\" />\n                        <span>共 {tokens.length} 个密钥</span>\n                      </div>\n                    )}\n                  </div>\n                  <button\n                    onClick={onClose}\n                    className=\"px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:border-gray-400 transition-colors\"\n                  >\n                    关闭\n                  </button>\n                </div>\n              </div>\n            </DialogPanel>\n          </TransitionChild>\n        </div>\n      </Dialog>\n    </Transition>\n  )\n}"
  },
  {
    "path": "components/DelAccountDialog.tsx",
    "content": "import { Fragment } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from \"@headlessui/react\"\nimport { ExclamationTriangleIcon, XMarkIcon, TrashIcon } from \"@heroicons/react/24/outline\"\nimport { accountStorage } from \"../services/accountStorage\"\nimport type { DisplaySiteData } from \"../types\"\n\ninterface DelAccountDialogProps {\n  isOpen: boolean\n  onClose: () => void\n  account: DisplaySiteData | null\n  onDeleted: () => void\n}\n\nexport default function DelAccountDialog({ isOpen, onClose, account, onDeleted }: DelAccountDialogProps) {\n  const handleDelete = async () => {\n    if (!account) {\n      return\n    }\n\n    try {\n      console.log('准备删除账号:', { id: account.id, name: account.name })\n      \n      await toast.promise(\n        accountStorage.deleteAccount(account.id),\n        {\n          loading: `正在删除账号 ${account.name}...`,\n          success: (success) => {\n            if (success) {\n              onDeleted()\n              onClose()\n              return `账号 ${account.name} 删除成功!`\n            } else {\n              throw new Error('删除失败')\n            }\n          },\n          error: (err) => {\n            const errorMsg = err instanceof Error ? err.message : '未知错误'\n            return `删除失败: ${errorMsg}`\n          },\n        }\n      )\n    } catch (error) {\n      console.error('删除账号失败:', error)\n    }\n  }\n\n  return (\n    <Transition show={isOpen} as={Fragment}>\n      <Dialog\n        onClose={onClose}\n        className=\"relative z-50\"\n      >\n        {/* 背景遮罩动画 */}\n        <TransitionChild\n          as={Fragment}\n          enter=\"ease-out duration-300\"\n          enterFrom=\"opacity-0\"\n          enterTo=\"opacity-100\"\n          leave=\"ease-in duration-200\"\n          leaveFrom=\"opacity-100\"\n          leaveTo=\"opacity-0\"\n        >\n          <div className=\"fixed inset-0 bg-black/30 backdrop-blur-sm\" aria-hidden=\"true\" />\n        </TransitionChild>\n        \n        {/* 居中容器 */}\n        <div className=\"fixed inset-0 flex items-center justify-center p-4\">\n          {/* 弹窗面板动画 */}\n          <TransitionChild\n            as={Fragment}\n            enter=\"ease-out duration-300\"\n            enterFrom=\"opacity-0 scale-95 translate-y-4\"\n            enterTo=\"opacity-100 scale-100 translate-y-0\"\n            leave=\"ease-in duration-200\"\n            leaveFrom=\"opacity-100 scale-100 translate-y-0\"\n            leaveTo=\"opacity-0 scale-95 translate-y-4\"\n          >\n            <DialogPanel className=\"w-full max-w-sm bg-white rounded-lg shadow-xl transform transition-all\">\n              {/* 头部 */}\n              <div className=\"flex items-center justify-between p-4 border-b border-gray-100\">\n                <div className=\"flex items-center space-x-3\">\n                  <div className=\"w-8 h-8 bg-gradient-to-r from-red-500 to-pink-600 rounded-lg flex items-center justify-center\">\n                    <TrashIcon className=\"w-4 h-4 text-white\" />\n                  </div>\n                  <DialogTitle className=\"text-lg font-semibold text-gray-900\">\n                    删除账号\n                  </DialogTitle>\n                </div>\n                <button\n                  onClick={onClose}\n                  className=\"p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors\"\n                >\n                  <XMarkIcon className=\"w-5 h-5\" />\n                </button>\n              </div>\n\n              {/* 内容区域 */}\n              <div className=\"p-4\">\n                {/* 警告图标和信息 */}\n                <div className=\"flex items-start space-x-3 mb-4\">\n                  <div className=\"flex-shrink-0\">\n                    <ExclamationTriangleIcon className=\"w-6 h-6 text-red-500\" />\n                  </div>\n                  <div className=\"flex-1\">\n                    <h3 className=\"text-sm font-medium text-gray-900 mb-2\">\n                      删除确认\n                    </h3>\n                    <p className=\"text-sm text-gray-500 mb-3\">\n                      您即将删除账号 <span className=\"font-medium text-gray-900\">{account?.name}</span>。\n                    </p>\n                    <p className=\"text-sm text-gray-500\">\n                      请核对后确认是否删除此账号\n                    </p>\n                  </div>\n                </div>\n\n                {/* 账号信息显示 */}\n                {account && (\n                  <div className=\"bg-gray-50 rounded-lg p-3 mb-4\">\n                    <div className=\"text-sm\">\n                      <div className=\"flex justify-between items-center mb-1\">\n                        <span className=\"text-gray-500\">站点名称：</span>\n                        <span className=\"font-medium text-gray-900\">{account.name}</span>\n                      </div>\n                      <div className=\"flex justify-between items-center mb-1\">\n                        <span className=\"text-gray-500\">用户名：</span>\n                        <span className=\"font-medium text-gray-900\">{account.username}</span>\n                      </div>\n                      <div className=\"flex justify-between items-center\">\n                        <span className=\"text-gray-500\">站点地址：</span>\n                        <span className=\"font-medium text-gray-900 truncate ml-2 max-w-48\" title={account.baseUrl}>\n                          {account.baseUrl}\n                        </span>\n                      </div>\n                    </div>\n                  </div>\n                )}\n\n                {/* 按钮组 */}\n                <div className=\"flex space-x-3\">\n                  <button\n                    type=\"button\"\n                    onClick={onClose}\n                    className=\"flex-1 px-4 py-2.5 text-sm font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500\"\n                  >\n                    取消\n                  </button>\n                  <button\n                    type=\"button\"\n                    onClick={handleDelete}\n                    className=\"flex-1 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-red-500 to-pink-600 rounded-lg hover:from-red-600 hover:to-pink-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 shadow-sm\"\n                  >\n                    确认删除\n                  </button>\n                </div>\n              </div>\n            </DialogPanel>\n          </TransitionChild>\n        </div>\n      </Dialog>\n    </Transition>\n  )\n}"
  },
  {
    "path": "components/EditAccountDialog.tsx",
    "content": "import { useState, useEffect, Fragment } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from \"@headlessui/react\"\nimport { GlobeAltIcon, XMarkIcon, PencilIcon, UserIcon, KeyIcon, EyeIcon, EyeSlashIcon, CurrencyDollarIcon, SparklesIcon, CheckIcon, UsersIcon } from \"@heroicons/react/24/outline\"\nimport { accountStorage } from \"../services/accountStorage\"\nimport { autoDetectAccount, validateAndUpdateAccount, extractDomainPrefix, isValidExchangeRate } from \"../services/accountOperations\"\nimport AutoDetectErrorAlert from \"./AutoDetectErrorAlert\"\nimport type { AutoDetectError } from \"../utils/autoDetectUtils\"\nimport type { DisplaySiteData } from \"../types\"\n\ninterface EditAccountDialogProps {\n  isOpen: boolean\n  onClose: () => void\n  account: DisplaySiteData | null\n}\n\nexport default function EditAccountDialog({ isOpen, onClose, account }: EditAccountDialogProps) {\n  const [url, setUrl] = useState(\"\")\n  const [isDetecting, setIsDetecting] = useState(false)\n  const [siteName, setSiteName] = useState(\"\")\n  const [username, setUsername] = useState(\"\")\n  const [accessToken, setAccessToken] = useState(\"\")\n  const [userId, setUserId] = useState(\"\")\n  const [isDetected, setIsDetected] = useState(false)\n  const [isSaving, setIsSaving] = useState(false)\n  const [showAccessToken, setShowAccessToken] = useState(false)\n  const [detectionError, setDetectionError] = useState<AutoDetectError | null>(null)\n  const [showManualForm, setShowManualForm] = useState(true) // 编辑模式默认显示表单\n  const [exchangeRate, setExchangeRate] = useState(\"\")\n  \n  // 重置表单数据\n  const resetForm = () => {\n    setUrl(\"\")\n    setIsDetected(false)\n    setSiteName(\"\")\n    setUsername(\"\")\n    setAccessToken(\"\")\n    setUserId(\"\")\n    setShowAccessToken(false)\n    setDetectionError(null)\n    setShowManualForm(true)\n    setExchangeRate(\"\")\n  }\n\n  // 加载账号数据到表单\n  const loadAccountData = async (accountId: string) => {\n    try {\n      const siteAccount = await accountStorage.getAccountById(accountId)\n      if (siteAccount) {\n        setUrl(siteAccount.site_url)\n        setSiteName(siteAccount.site_name)\n        setUsername(siteAccount.account_info.username)\n        setAccessToken(siteAccount.account_info.access_token)\n        setUserId(siteAccount.account_info.id.toString())\n        setExchangeRate(siteAccount.exchange_rate.toString())\n      }\n    } catch (error) {\n      console.error('加载账号数据失败:', error)\n    }\n  }\n\n  useEffect(() => {\n    if (isOpen && account) {\n      resetForm()\n      loadAccountData(account.id)\n    } else if (!isOpen) {\n      resetForm()\n    }\n  }, [isOpen, account])\n\n  const handleAutoDetect = async () => {\n    if (!url.trim()) {\n      return\n    }\n\n    setIsDetecting(true)\n    setDetectionError(null)\n    \n    try {\n      const result = await autoDetectAccount(url.trim())\n      \n      if (!result.success) {\n        setDetectionError(result.detailedError || null)\n        return\n      }\n\n      if (result.data) {\n        // 更新表单数据\n        setUsername(result.data.username)\n        setAccessToken(result.data.accessToken)\n        setUserId(result.data.userId)\n        \n        // 设置充值比例默认值\n        if (result.data.exchangeRate) {\n          setExchangeRate(result.data.exchangeRate.toString())\n          console.log('获取到默认充值比例:', result.data.exchangeRate)\n        } else {\n          console.log('未获取到默认充值比例，保持当前值')\n        }\n        \n        setIsDetected(true)\n        \n        console.log('自动识别成功:', { \n          username: result.data.username, \n          siteName, \n          exchangeRate: result.data.exchangeRate \n        })\n      }\n    } catch (error) {\n      console.error('自动识别失败:', error)\n      const errorMessage = error instanceof Error ? error.message : '未知错误'\n      // 使用通用错误处理\n      setDetectionError({\n        type: 'unknown' as any,\n        message: `自动识别失败: ${errorMessage}`,\n        helpDocUrl: '#'\n      })\n    } finally {\n      setIsDetecting(false)\n    }\n  }\n\n  const handleSaveAccount = async () => {\n    if (!account) {\n      toast.error('账号信息错误')\n      return\n    }\n\n    setIsSaving(true)\n    \n    try {\n      await toast.promise(\n        validateAndUpdateAccount(\n          account.id,\n          url.trim(),\n          siteName.trim(),\n          username.trim(),\n          accessToken.trim(),\n          userId.trim(),\n          exchangeRate\n        ),\n        {\n          loading: '正在保存更改...',\n          success: (result) => {\n            if (result.success) {\n              onClose()\n              return `账号 ${siteName} 更新成功!`\n            } else {\n              throw new Error(result.error || '更新失败')\n            }\n          },\n          error: (err) => {\n            const errorMsg = err.message || '更新失败'\n            return `更新失败: ${errorMsg}`\n          },\n        }\n      )\n    } catch (error) {\n      console.error('更新账号失败:', error)\n    } finally {\n      setIsSaving(false)\n    }\n  }\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault()\n    if (isDetected || showManualForm) {\n      handleSaveAccount()\n    } else {\n      handleAutoDetect()\n    }\n  }\n\n  return (\n    <Transition show={isOpen} as={Fragment}>\n      <Dialog\n        onClose={onClose}\n        className=\"relative z-50\"\n      >\n        {/* 背景遮罩动画 */}\n        <TransitionChild\n          as={Fragment}\n          enter=\"ease-out duration-300\"\n          enterFrom=\"opacity-0\"\n          enterTo=\"opacity-100\"\n          leave=\"ease-in duration-200\"\n          leaveFrom=\"opacity-100\"\n          leaveTo=\"opacity-0\"\n        >\n          <div className=\"fixed inset-0 bg-black/30 backdrop-blur-sm\" aria-hidden=\"true\" />\n        </TransitionChild>\n        \n        {/* 居中容器 - 针对插件优化 */}\n        <div className=\"fixed inset-0 flex items-center justify-center p-2\">\n          {/* 弹窗面板动画 */}\n          <TransitionChild\n            as={Fragment}\n            enter=\"ease-out duration-300\"\n            enterFrom=\"opacity-0 scale-95 translate-y-4\"\n            enterTo=\"opacity-100 scale-100 translate-y-0\"\n            leave=\"ease-in duration-200\"\n            leaveFrom=\"opacity-100 scale-100 translate-y-0\"\n            leaveTo=\"opacity-0 scale-95 translate-y-4\"\n          >\n            <DialogPanel className=\"w-full max-w-sm bg-white rounded-lg shadow-xl transform transition-all max-h-[90vh] overflow-y-auto\">\n              {/* 头部 */}\n              <div className=\"flex items-center justify-between p-4 border-b border-gray-100\">\n                <div className=\"flex items-center space-x-3\">\n                  <div className=\"w-8 h-8 bg-gradient-to-r from-green-500 to-emerald-600 rounded-lg flex items-center justify-center\">\n                    <PencilIcon className=\"w-4 h-4 text-white\" />\n                  </div>\n                  <DialogTitle className=\"text-lg font-semibold text-gray-900\">\n                    编辑账号\n                  </DialogTitle>\n                </div>\n                <button\n                  onClick={onClose}\n                  className=\"p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors\"\n                >\n                  <XMarkIcon className=\"w-5 h-5\" />\n                </button>\n              </div>\n\n              {/* 内容区域 */}\n              <div className=\"p-4\">\n                <form onSubmit={handleSubmit} className=\"space-y-6\">\n                  {/* 识别错误提示 */}\n                  {detectionError && (\n                    <AutoDetectErrorAlert \n                      error={detectionError}\n                      siteUrl={url}\n                    />\n                  )}\n\n                  {/* URL 输入框 */}\n                  <div>\n                    <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                      站点地址\n                    </label>\n                    <div className=\"relative\">\n                      <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                        <GlobeAltIcon className=\"h-5 w-5 text-gray-400\" />\n                      </div>\n                      <input\n                        type=\"url\"\n                        value={url}\n                        onChange={(e) => {\n                          const inputUrl = e.target.value\n                          \n                          // 当用户输入 URL 时，提取协议和主机部分\n                          if (inputUrl.trim()) {\n                            try {\n                              const urlObj = new URL(inputUrl)\n                              // 只保留协议和主机部分，不带路径\n                              const baseUrl = `${urlObj.protocol}//${urlObj.host}`\n                              setUrl(baseUrl)\n                              \n                              // 自动更新站点名称\n                              const domainPrefix = extractDomainPrefix(urlObj.hostname)\n                              setSiteName(domainPrefix)\n                            } catch (error) {\n                              // 如果 URL 格式不完整，先保存用户输入，但尝试提取域名\n                              setUrl(inputUrl)\n                              const match = inputUrl.match(/\\/\\/([^\\/]+)/)\n                              if (match) {\n                                const domainPrefix = extractDomainPrefix(match[1])\n                                setSiteName(domainPrefix)\n                              }\n                            }\n                          } else {\n                            setUrl(\"\")\n                            setSiteName(\"\")\n                          }\n                        }}\n                        placeholder=\"https://example.com\"\n                        className=\"block w-full pl-10 pr-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors\"\n                        required\n                        disabled={isDetected}\n                      />\n                      {url && (\n                        <button\n                          type=\"button\"\n                          onClick={() => setUrl('')}\n                          className=\"absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors\"\n                          disabled={isDetected}\n                        >\n                          <XMarkIcon className=\"h-4 w-4\" />\n                        </button>\n                      )}\n                    </div>\n                    <p className=\"mt-2 text-xs text-gray-500\">\n                      请输入 One API 或 New API 站点的完整地址\n                    </p>\n                  </div>\n\n                  {/* 账号信息表单 */}\n                  <div className=\"space-y-6\">\n                    {/* 网站名称 */}\n                    <div>\n                      <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                        网站名称\n                      </label>\n                      <div className=\"relative\">\n                        <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                          <GlobeAltIcon className=\"h-5 w-5 text-gray-400\" />\n                        </div>\n                        <input\n                          type=\"text\"\n                          value={siteName}\n                          onChange={(e) => setSiteName(e.target.value)}\n                          placeholder=\"example.com\"\n                          className=\"block w-full pl-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors\"\n                          required\n                        />\n                      </div>\n                    </div>\n\n                    {/* 用户名 */}\n                    <div>\n                      <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                        用户名\n                      </label>\n                      <div className=\"relative\">\n                        <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                          <UserIcon className=\"h-5 w-5 text-gray-400\" />\n                        </div>\n                        <input\n                          type=\"text\"\n                          value={username}\n                          onChange={(e) => setUsername(e.target.value)}\n                          placeholder=\"用户名\"\n                          className=\"block w-full pl-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors\"\n                          required\n                        />\n                      </div>\n                    </div>\n\n                    {/* 用户 ID */}\n                    <div>\n                      <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                        用户 ID\n                      </label>\n                      <div className=\"relative\">\n                        <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                          <span className=\"text-gray-400 font-mono text-sm\">#</span>\n                        </div>\n                        <input\n                          type=\"number\"\n                          value={userId}\n                          onChange={(e) => setUserId(e.target.value)}\n                          placeholder=\"用户 ID (数字)\"\n                          className=\"block w-full pl-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors\"\n                          required\n                        />\n                      </div>\n                    </div>\n\n                    {/* 访问令牌 */}\n                    <div>\n                      <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                        访问令牌\n                      </label>\n                      <div className=\"relative\">\n                        <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                          <KeyIcon className=\"h-5 w-5 text-gray-400\" />\n                        </div>\n                        <input\n                          type={showAccessToken ? \"text\" : \"password\"}\n                          value={accessToken}\n                          onChange={(e) => setAccessToken(e.target.value)}\n                          placeholder=\"访问令牌\"\n                          className=\"block w-full pl-10 pr-10 py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors\"\n                          required\n                        />\n                        <button\n                          type=\"button\"\n                          onClick={() => setShowAccessToken(!showAccessToken)}\n                          className=\"absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors\"\n                        >\n                          {showAccessToken ? (\n                            <EyeSlashIcon className=\"h-4 w-4\" />\n                          ) : (\n                            <EyeIcon className=\"h-4 w-4\" />\n                          )}\n                        </button>\n                      </div>\n                    </div>\n\n                    {/* 充值金额比例 */}\n                    <div>\n                      <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                        充值金额比例 (CNY/USD)\n                      </label>\n                      <div className=\"relative\">\n                        <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n                          <CurrencyDollarIcon className=\"h-5 w-5 text-gray-400\" />\n                        </div>\n                        <input\n                          type=\"number\"\n                          step=\"0.1\"\n                          min=\"0.1\"\n                          max=\"100\"\n                          value={exchangeRate}\n                          onChange={(e) => setExchangeRate(e.target.value)}\n                          placeholder=\"请输入充值比例\"\n                          className={`block w-full pl-10 py-3 border rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 transition-colors ${\n                            isValidExchangeRate(exchangeRate) \n                              ? 'border-gray-200 focus:ring-green-500 focus:border-transparent' \n                              : 'border-red-300 focus:ring-red-500 focus:border-red-500'\n                          }`}\n                          required\n                        />\n                        <div className=\"absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none\">\n                          <span className=\"text-sm text-gray-500\">CNY</span>\n                        </div>\n                      </div>\n                      <p className=\"mt-1 text-xs text-gray-500\">\n                        表示充值 1 美元需要多少人民币\n                      </p>\n                      {!isValidExchangeRate(exchangeRate) && exchangeRate && (\n                        <p className=\"mt-1 text-xs text-red-600\">\n                          请输入有效的汇率 (0.1 - 100)\n                        </p>\n                      )}\n                    </div>\n                  </div>\n\n                  {/* 按钮组 */}\n                  <div className=\"flex space-x-3 pt-2\">\n                    <button\n                      type=\"button\"\n                      onClick={onClose}\n                      className=\"px-4 py-2.5 text-sm font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500\"\n                    >\n                      取消\n                    </button>\n                    \n                    {/* 重新识别按钮 */}\n                    {!isDetected && (\n                      <button\n                        type=\"button\"\n                        onClick={handleAutoDetect}\n                        disabled={!url.trim() || isDetecting}\n                        className=\"flex-1 flex items-center justify-center space-x-2 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-indigo-600 rounded-lg hover:from-blue-600 hover:to-indigo-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm\"\n                      >\n                        {isDetecting ? (\n                          <>\n                            <div className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n                            <span>识别中...</span>\n                          </>\n                        ) : (\n                          <>\n                            <SparklesIcon className=\"w-4 h-4\" />\n                            <span>重新识别</span>\n                          </>\n                        )}\n                      </button>\n                    )}\n                    \n                    {/* 保存按钮 */}\n                    <button\n                      type=\"submit\"\n                      disabled={!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim() || !isValidExchangeRate(exchangeRate) || isSaving}\n                      className=\"flex-1 flex items-center justify-center space-x-2 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm\"\n                    >\n                      {isSaving ? (\n                        <>\n                          <div className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n                          <span>保存中...</span>\n                        </>\n                      ) : (\n                        <>\n                          <CheckIcon className=\"w-4 h-4\" />\n                          <span>保存更改</span>\n                        </>\n                      )}\n                    </button>\n                  </div>\n                </form>\n              </div>\n\n              {/* 提示信息 */}\n              <div className=\"px-4 pb-4\">\n                <div className=\"bg-green-50 border border-green-100 rounded-lg p-3\">\n                  <div className=\"flex\">\n                    <div className=\"flex-shrink-0\">\n                      <UsersIcon className=\"h-5 w-5 text-green-400\" />\n                    </div>\n                    <div className=\"ml-3\">\n                      <h3 className=\"text-xs font-medium text-green-800\">\n                        编辑账号信息\n                      </h3>\n                      <div className=\"mt-1 text-xs text-green-700\">\n                        <p>\n                          修改账号信息后，系统会重新验证并获取最新的余额数据。\n                        </p>\n                        <p>\n                          如果站点信息有变化，建议点击\"重新识别\"按钮（需要在目标站点先自行登录）\n                        </p>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </DialogPanel>\n          </TransitionChild>\n        </div>\n      </Dialog>\n    </Transition>\n  )\n}"
  },
  {
    "path": "components/HeaderSection.tsx",
    "content": "import { ArrowsPointingOutIcon, Cog6ToothIcon, ArrowPathIcon } from \"@heroicons/react/24/outline\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport Tooltip from \"./Tooltip\"\nimport iconImage from \"../assets/icon.png\"\n\ninterface HeaderSectionProps {\n  isRefreshing: boolean\n  onRefresh: () => void\n  onOpenTab: () => void\n  onOpenSettings: () => void\n}\n\nexport default function HeaderSection({ \n  isRefreshing, \n  onRefresh, \n  onOpenTab,\n  onOpenSettings\n}: HeaderSectionProps) {\n  return (\n    <div className=\"flex items-center justify-between px-5 py-4 bg-white border-b border-gray-100 flex-shrink-0\">\n      <div className=\"flex items-center space-x-3\">\n        <img \n          src={iconImage} \n          alt=\"One API Hub\" \n          className=\"w-7 h-7 rounded-lg shadow-sm\"\n        />\n        <div className=\"flex flex-col\">\n          <span className=\"font-semibold text-gray-900\">One API Hub</span>\n          <span className=\"text-xs text-gray-500\">一键管理所有AI中转站</span>\n        </div>\n      </div>\n      \n      <div className=\"flex items-center space-x-2\">\n        <Tooltip content=\"刷新数据\">\n          <button\n            onClick={onRefresh}\n            disabled={isRefreshing}\n            className={`${UI_CONSTANTS.STYLES.BUTTON.ICON} ${isRefreshing ? 'animate-spin' : ''}`}\n            title=\"刷新数据\"\n          >\n            <ArrowPathIcon className=\"w-4 h-4\" />\n          </button>\n        </Tooltip>\n        <button\n          onClick={onOpenTab}\n          className={UI_CONSTANTS.STYLES.BUTTON.ICON}\n          title=\"打开完整管理页面\"\n        >\n          <ArrowsPointingOutIcon className=\"w-4 h-4\" />\n        </button>\n        <button\n          onClick={onOpenSettings}\n          className={UI_CONSTANTS.STYLES.BUTTON.ICON}\n          title=\"设置\"\n        >\n          <Cog6ToothIcon className=\"w-4 h-4\" />\n        </button>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "components/ModelItem.tsx",
    "content": "/**\n * 模型列表项组件\n */\n\nimport React, { useState } from 'react'\nimport { \n  DocumentDuplicateIcon, \n  ChevronDownIcon, \n  ChevronUpIcon,\n  TagIcon,\n  CurrencyDollarIcon,\n  ServerIcon\n} from '@heroicons/react/24/outline'\nimport toast from 'react-hot-toast'\nimport type { ModelPricing } from '../services/apiService'\nimport type { CalculatedPrice } from '../utils/modelPricing'\nimport { \n  getProviderConfig, \n  type ProviderType \n} from '../utils/modelProviders'\nimport { \n  formatPrice, \n  formatPriceCompact, \n  getBillingModeText, \n  getBillingModeStyle,\n  getEndpointTypesText \n} from '../utils/modelPricing'\n\ninterface ModelItemProps {\n  model: ModelPricing\n  calculatedPrice: CalculatedPrice\n  exchangeRate: number\n  showRealPrice: boolean // 是否以真实充值金额展示\n  showRatioColumn: boolean // 是否显示倍率列\n  showEndpointTypes: boolean // 是否显示可用端点类型\n  userGroup: string\n  onGroupClick?: (group: string) => void // 新增：点击分组时的回调函数\n  availableGroups?: string[] // 新增：用户的所有可用分组列表\n  isAllGroupsMode?: boolean // 新增：是否为\"所有分组\"模式\n}\n\nexport default function ModelItem({\n  model,\n  calculatedPrice,\n  exchangeRate,\n  showRealPrice,\n  showRatioColumn,\n  showEndpointTypes,\n  userGroup,\n  onGroupClick,\n  availableGroups = [],\n  isAllGroupsMode = false\n}: ModelItemProps) {\n  const [isExpanded, setIsExpanded] = useState(false)\n  \n  // 获取厂商配置\n  const providerConfig = getProviderConfig(model.model_name)\n  const IconComponent = providerConfig.icon\n  \n  // 获取计费模式样式\n  const billingStyle = getBillingModeStyle(model.quota_type)\n  \n  // 检查模型是否对当前用户分组可用\n  const isAvailableForUser = isAllGroupsMode \n    ? availableGroups.some(group => model.enable_groups.includes(group)) // 所有分组模式：任何一个用户分组可用即可\n    : model.enable_groups.includes(userGroup) // 特定分组模式：必须该分组可用\n  \n  // 复制模型名称\n  const handleCopyModelName = async () => {\n    try {\n      await navigator.clipboard.writeText(model.model_name)\n      toast.success('模型名称已复制')\n    } catch (error) {\n      toast.error('复制失败')\n    }\n  }\n  \n  return (\n    <div className={`border rounded-lg transition-all duration-200 ${\n      isAvailableForUser \n        ? 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm' \n        : 'border-gray-100 bg-gray-50'\n    }`}>\n      {/* 主要信息行 */}\n      <div className=\"p-4\">\n        <div className=\"flex items-start justify-between\">\n          {/* 左侧：模型名称和基本信息 */}\n          <div className=\"flex-1 min-w-0\">\n            <div className=\"flex items-center space-x-3 mb-2\">\n              {/* 厂商图标 */}\n              <div className={`p-1.5 rounded-lg ${providerConfig.bgColor}`}>\n                <IconComponent className={`w-4 h-4 ${providerConfig.color}`} />\n              </div>\n              \n              {/* 模型名称 */}\n              <div className=\"flex items-center space-x-2 min-w-0\">\n                <h3 className={`text-lg font-semibold ${\n                  isAvailableForUser ? 'text-gray-900' : 'text-gray-500'\n                }`}>\n                  {model.model_name}\n                </h3>\n                \n                {/* 复制按钮 */}\n                <button\n                  onClick={handleCopyModelName}\n                  className=\"p-1 hover:bg-gray-100 rounded transition-colors\"\n                  title=\"复制模型名称\"\n                >\n                  <DocumentDuplicateIcon className=\"w-3 h-3 text-gray-400\" />\n                </button>\n              </div>\n              \n              {/* 计费模式标签 */}\n              <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${billingStyle.color} ${billingStyle.bgColor}`}>\n                {getBillingModeText(model.quota_type)}\n              </span>\n              \n              {/* 可用状态标签 */}\n              <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${\n                isAvailableForUser \n                  ? 'bg-green-100 text-green-800' \n                  : 'bg-gray-100 text-gray-600'\n              }`}>\n                {isAvailableForUser ? '可用' : '不可用'}\n              </span>\n            </div>\n            \n            {/* 模型描述 */}\n            {model.model_description && (\n              <div className=\"mb-2\">\n                <p className={`text-sm leading-relaxed ${\n                  isAvailableForUser ? 'text-gray-600' : 'text-gray-400'\n                } overflow-hidden`} \n                style={{\n                  display: '-webkit-box',\n                  WebkitLineClamp: 2,\n                  WebkitBoxOrient: 'vertical'\n                }}\n                title={model.model_description}>\n                  {model.model_description}\n                </p>\n              </div>\n            )}\n            \n            {/* 价格信息 */}\n            <div className=\"mt-2\">\n              {model.quota_type === 0 ? (\n                // 按量计费 - 横向并排显示价格\n                <div className=\"flex items-center gap-6\">\n                  {/* 输入价格 */}\n                  <div className=\"flex items-center space-x-2\">\n                    <span className=\"text-sm text-gray-600\">输入:</span>\n                    <span className={`text-sm ${\n                      isAvailableForUser ? 'text-blue-600' : 'text-gray-500'\n                    }`}>\n                      {showRealPrice \n                        ? `${formatPriceCompact(calculatedPrice.inputCNY, 'CNY')}/M`\n                        : `${formatPriceCompact(calculatedPrice.inputUSD, 'USD')}/M`\n                      }\n                    </span>\n                  </div>\n                  \n                  {/* 输出价格 */}\n                  <div className=\"flex items-center space-x-2\">\n                    <span className=\"text-sm text-gray-600\">输出:</span>\n                    <span className={`text-sm ${\n                      isAvailableForUser ? 'text-green-600' : 'text-gray-500'\n                    }`}>\n                      {showRealPrice \n                        ? `${formatPriceCompact(calculatedPrice.outputCNY, 'CNY')}/M`\n                        : `${formatPriceCompact(calculatedPrice.outputUSD, 'USD')}/M`\n                      }\n                    </span>\n                  </div>\n                  \n                  {/* 倍率显示 */}\n                  {showRatioColumn && (\n                    <div className=\"flex items-center space-x-2\">\n                      <span className=\"text-sm text-gray-500\">倍率:</span>\n                      <span className={`text-sm font-medium ${\n                        isAvailableForUser ? 'text-gray-900' : 'text-gray-500'\n                      }`}>\n                        {model.model_ratio}x\n                      </span>\n                    </div>\n                  )}\n                </div>\n              ) : (\n                // 按次计费\n                <div className=\"flex items-center space-x-2\">\n                  <span className=\"text-sm text-gray-600\">每次调用:</span>\n                  <span className={`text-sm ${\n                    isAvailableForUser ? 'text-purple-600' : 'text-gray-500'\n                  }`}>\n                    {showRealPrice \n                      ? formatPriceCompact((calculatedPrice.perCallPrice || 0) * exchangeRate, 'CNY')\n                      : formatPriceCompact(calculatedPrice.perCallPrice || 0, 'USD')\n                    }\n                  </span>\n                </div>\n              )}\n            </div>\n          </div>\n          \n          {/* 右侧：展开/收起按钮 */}\n          <button\n            onClick={() => setIsExpanded(!isExpanded)}\n            className=\"ml-4 p-2 hover:bg-gray-100 rounded-lg transition-colors\"\n            title={isExpanded ? '收起详细信息' : '展开详细信息'}\n          >\n            {isExpanded ? (\n              <ChevronUpIcon className=\"w-4 h-4 text-gray-400\" />\n            ) : (\n              <ChevronDownIcon className=\"w-4 h-4 text-gray-400\" />\n            )}\n          </button>\n        </div>\n      </div>\n      \n      {/* 展开的详细信息 */}\n      {isExpanded && (\n        <div className=\"border-t border-gray-100 px-4 py-3\">\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm\">\n            {/* 可用分组 */}\n            <div>\n              <div className=\"flex items-center space-x-2 mb-2\">\n                <TagIcon className=\"w-4 h-4 text-gray-400\" />\n                <span className=\"font-medium text-gray-700\">可用分组</span>\n              </div>\n              <div className=\"flex flex-wrap gap-1\">\n                {model.enable_groups.map((group, index) => {\n                  const isCurrentGroup = group === userGroup\n                  const isClickable = onGroupClick && !isCurrentGroup\n                  \n                  return (\n                    <span \n                      key={index}\n                      onClick={isClickable ? () => onGroupClick(group) : undefined}\n                      className={`inline-flex items-center px-2 py-1 rounded text-xs cursor-pointer transition-colors ${\n                        isCurrentGroup\n                          ? 'bg-blue-100 text-blue-800 font-medium' \n                          : isClickable\n                          ? 'bg-gray-100 text-gray-600 hover:bg-blue-50 hover:text-blue-700' \n                          : 'bg-gray-100 text-gray-600'\n                      }`}\n                      title={isClickable ? `点击切换到 ${group} 分组` : undefined}\n                    >\n                      {isCurrentGroup && <TagIcon className=\"w-3 h-3 mr-1\" />}\n                      {group}\n                    </span>\n                  )\n                })}\n              </div>\n            </div>\n            \n            {/* 可用端点类型 */}\n            {showEndpointTypes && (\n              <div>\n                <div className=\"flex items-center space-x-2 mb-2\">\n                  <ServerIcon className=\"w-4 h-4 text-gray-400\" />\n                  <span className=\"font-medium text-gray-700\">端点类型</span>\n                </div>\n                <div className=\"text-gray-600\">\n                  {getEndpointTypesText(model.supported_endpoint_types)}\n                </div>\n              </div>\n            )}\n            \n            {/* 详细定价信息（仅按量计费模型） */}\n            {model.quota_type === 0 && (\n              <div className=\"md:col-span-2\">\n                <div className=\"flex items-center space-x-2 mb-2\">\n                  <CurrencyDollarIcon className=\"w-4 h-4 text-gray-400\" />\n                  <span className=\"font-medium text-gray-700\">详细定价</span>\n                </div>\n                <div className=\"grid grid-cols-2 gap-4 text-xs\">\n                  <div className=\"space-y-1\">\n                    <div className=\"text-gray-500\">输入(1M tokens)</div>\n                    <div className=\"font-medium\">\n                      USD: {formatPrice(calculatedPrice.inputUSD, 'USD')}\n                    </div>\n                    <div className=\"font-medium\">\n                      CNY: {formatPrice(calculatedPrice.inputCNY, 'CNY')}\n                    </div>\n                  </div>\n                  <div className=\"space-y-1\">\n                    <div className=\"text-gray-500\">输出(1M tokens)</div>\n                    <div className=\"font-medium\">\n                      USD: {formatPrice(calculatedPrice.outputUSD, 'USD')}\n                    </div>\n                    <div className=\"font-medium\">\n                      CNY: {formatPrice(calculatedPrice.outputCNY, 'CNY')}\n                    </div>\n                  </div>\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}"
  },
  {
    "path": "components/Tooltip.tsx",
    "content": "import { useState, useRef, useEffect } from \"react\"\nimport type { ReactNode } from \"react\"\n\ninterface TooltipProps {\n  content: ReactNode\n  children: ReactNode\n  position?: 'top' | 'bottom' | 'left' | 'right' | 'auto'\n  className?: string\n  delay?: number\n}\n\nexport default function Tooltip({ \n  content, \n  children, \n  position = 'auto', \n  className = '',\n  delay = 0 \n}: TooltipProps) {\n  const [showTooltip, setShowTooltip] = useState(false)\n  const [actualPosition, setActualPosition] = useState<'top' | 'bottom' | 'left' | 'right'>('top')\n  const containerRef = useRef<HTMLDivElement>(null)\n  const tooltipRef = useRef<HTMLDivElement>(null)\n\n  const handleMouseEnter = () => {\n    if (delay > 0) {\n      setTimeout(() => setShowTooltip(true), delay)\n    } else {\n      setShowTooltip(true)\n    }\n  }\n\n  const handleMouseLeave = () => {\n    setShowTooltip(false)\n  }\n\n  // 检测最佳位置\n  useEffect(() => {\n    if (showTooltip && position === 'auto' && containerRef.current && tooltipRef.current) {\n      const container = containerRef.current\n      const tooltip = tooltipRef.current\n      const containerRect = container.getBoundingClientRect()\n      const tooltipRect = tooltip.getBoundingClientRect()\n      \n      // 插件窗口的边界（假设宽度为384px，高度为600px）\n      const windowWidth = 384\n      const windowHeight = 600\n      \n      let bestPosition: 'top' | 'bottom' | 'left' | 'right' = 'top'\n      \n      // 检查是否有足够空间显示在上方\n      if (containerRect.top > tooltipRect.height + 10) {\n        bestPosition = 'top'\n      }\n      // 检查是否有足够空间显示在下方\n      else if (containerRect.bottom + tooltipRect.height + 10 < windowHeight) {\n        bestPosition = 'bottom'\n      }\n      // 检查是否有足够空间显示在左侧\n      else if (containerRect.left > tooltipRect.width + 10) {\n        bestPosition = 'left'\n      }\n      // 检查是否有足够空间显示在右侧\n      else if (containerRect.right + tooltipRect.width + 10 < windowWidth) {\n        bestPosition = 'right'\n      }\n      // 默认显示在上方，即使空间不足\n      else {\n        bestPosition = 'top'\n      }\n      \n      setActualPosition(bestPosition)\n    } else if (position !== 'auto') {\n      setActualPosition(position)\n    }\n  }, [showTooltip, position])\n\n  const getPositionClasses = () => {\n    switch (actualPosition) {\n      case 'top':\n        return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2'\n      case 'bottom':\n        return 'top-full left-1/2 transform -translate-x-1/2 mt-2'\n      case 'left':\n        return 'right-full top-1/2 transform -translate-y-1/2 mr-2'\n      case 'right':\n        return 'left-full top-1/2 transform -translate-y-1/2 ml-2'\n      default:\n        return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2'\n    }\n  }\n\n  const getArrowClasses = () => {\n    switch (actualPosition) {\n      case 'top':\n        return 'absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-2 border-r-2 border-t-4 border-transparent border-t-gray-900'\n      case 'bottom':\n        return 'absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-2 border-r-2 border-b-4 border-transparent border-b-gray-900'\n      case 'left':\n        return 'absolute left-full top-1/2 transform -translate-y-1/2 w-0 h-0 border-t-2 border-b-2 border-l-4 border-transparent border-l-gray-900'\n      case 'right':\n        return 'absolute right-full top-1/2 transform -translate-y-1/2 w-0 h-0 border-t-2 border-b-2 border-r-4 border-transparent border-r-gray-900'\n      default:\n        return 'absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-2 border-r-2 border-t-4 border-transparent border-t-gray-900'\n    }\n  }\n\n  return (\n    <div className=\"relative w-fit\" ref={containerRef}>\n      <div\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n      >\n        {children}\n      </div>\n      \n      <div \n        ref={tooltipRef}\n        className={`absolute ${getPositionClasses()} px-3 py-2 bg-gray-900 text-white text-xs rounded-lg shadow-lg z-[9999] whitespace-nowrap transition-all duration-200 ${showTooltip ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1 pointer-events-none'} ${className}`}\n      >\n        {content}\n        <div className={getArrowClasses()}></div>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "constants/ui.ts",
    "content": "/**\n * UI 相关常量定义\n */\n\nexport const UI_CONSTANTS = {\n  // 弹窗尺寸\n  POPUP: {\n    WIDTH: 'w-96',\n    HEIGHT: 'h-[600px]',\n    MAX_HEIGHT: 'max-h-[90vh]'\n  },\n\n  // 动画配置\n  ANIMATION: {\n    INITIAL_DURATION: 1.5,\n    UPDATE_DURATION: 0.8,\n    FAST_DURATION: 0.6,\n    SLOW_DURATION: 1.0\n  },\n\n  // 更新间隔\n  UPDATE_INTERVAL: 30000, // 30秒\n\n  // 排序相关\n  SORT: {\n    DEFAULT_FIELD: 'balance' as const,\n    DEFAULT_ORDER: 'desc' as const\n  },\n\n  // Token 格式化阈值\n  TOKEN: {\n    MILLION_THRESHOLD: 1000000,\n    THOUSAND_THRESHOLD: 1000\n  },\n\n  // 汇率相关\n  EXCHANGE_RATE: {\n    DEFAULT: 7.2,\n    CONVERSION_FACTOR: 500000 // USD to quota conversion\n  },\n\n  // 样式类名\n  STYLES: {\n    // 按钮样式\n    BUTTON: {\n      PRIMARY: 'flex-1 flex items-center justify-center space-x-2 py-2.5 px-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors text-sm font-medium shadow-sm',\n      SECONDARY: 'flex items-center justify-center py-2.5 px-3 bg-white text-gray-600 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium border border-gray-200',\n      ICON: 'p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-all duration-200'\n    },\n    \n    // 状态指示器\n    STATUS_INDICATOR: {\n      HEALTHY: 'bg-green-500',\n      ERROR: 'bg-red-500',\n      WARNING: 'bg-yellow-500',\n      UNKNOWN: 'bg-gray-400'\n    },\n\n    // 文本颜色\n    TEXT: {\n      PRIMARY: 'text-gray-900',\n      SECONDARY: 'text-gray-500',\n      SUCCESS: 'text-green-500',\n      ERROR: 'text-red-500',\n      WARNING: 'text-yellow-500'\n    },\n\n    // 输入框\n    INPUT: {\n      BASE: 'block w-full py-3 border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors',\n      WITH_ICON: 'pl-10'\n    }\n  }\n} as const\n\nexport const CURRENCY_SYMBOLS = {\n  USD: '$',\n  CNY: '¥'\n} as const\n\nexport const HEALTH_STATUS_MAP = {\n  healthy: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.HEALTHY, text: '正常' },\n  error: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.ERROR, text: '错误' },\n  warning: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.WARNING, text: '警告' },\n  unknown: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.UNKNOWN, text: '未知' }\n} as const"
  },
  {
    "path": "content.ts",
    "content": "import type { PlasmoCSConfig } from \"plasmo\"\n\nimport { fetchUserInfo } from \"~services/apiService\"\n\nexport const config: PlasmoCSConfig = {\n  matches: [\"<all_urls>\"],\n  all_frames: false\n}\n\n// 监听来自 popup 和 background 的消息\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n  if (request.action === \"getLocalStorage\") {\n    try {\n      const { key } = request\n\n      if (key) {\n        // 读取特定键\n        const value = localStorage.getItem(key)\n        sendResponse({ success: true, data: { [key]: value } })\n      } else {\n        // 读取所有 localStorage 数据\n        const localStorage = window.localStorage\n        const data = {}\n\n        for (let i = 0; i < localStorage.length; i++) {\n          const storageKey = localStorage.key(i)\n          if (storageKey) {\n            data[storageKey] = localStorage.getItem(storageKey)\n          }\n        }\n\n        sendResponse({ success: true, data })\n      }\n    } catch (error) {\n      sendResponse({ success: false, error: error.message })\n    }\n    return true // 保持消息通道开放\n  }\n\n  if (request.action === \"getUserFromLocalStorage\") {\n    ;(async () => {\n      try {\n        // 所有异步逻辑\n        const userStr = localStorage.getItem(\"user\")\n        let user = userStr\n          ? JSON.parse(userStr)\n          : await fetchUserInfo(request.url)\n\n        if (!user || !user.id) {\n          sendResponse({\n            success: false,\n            error: \"未找到用户信息，请确保已登录\"\n          })\n          return\n        }\n\n        sendResponse({ success: true, data: { userId: user.id, user } })\n      } catch (e) {\n        sendResponse({ success: false, error: e.message })\n      }\n    })()\n    return true\n  }\n\n  if (request.action === \"waitAndGetUserInfo\") {\n    // 新增：等待页面完全加载后获取用户信息\n    waitForUserInfo()\n      .then((userInfo) => {\n        sendResponse({ success: true, data: userInfo })\n      })\n      .catch((error) => {\n        sendResponse({ success: false, error: error.message })\n      })\n    return true\n  }\n})\n\n// 等待用户信息可用\nasync function waitForUserInfo(\n  maxWaitTime = 5000\n): Promise<{ userId: string; user: any }> {\n  const startTime = Date.now()\n\n  while (Date.now() - startTime < maxWaitTime) {\n    try {\n      const userStr = localStorage.getItem(\"user\")\n      if (userStr) {\n        const user = JSON.parse(userStr)\n        if (user.id) {\n          return { userId: user.id, user }\n        }\n      }\n    } catch (error) {\n      // 继续等待\n    }\n\n    // 等待 100ms 后重试\n    await new Promise((resolve) => setTimeout(resolve, 100))\n  }\n\n  throw new Error(\"等待用户信息超时，请确保已登录\")\n}\n"
  },
  {
    "path": "debug/testStorage.ts",
    "content": "import { accountStorage } from \"../services/accountStorage\";\n\n/**\n * 存储功能测试脚本\n */\nexport const testStorageFunction = async () => {\n  console.log('=== 开始测试存储功能 ===');\n\n  try {\n    // 测试添加账号\n    const testAccount = {\n      emoji: \"\",\n      site_name: \"测试站点\",\n      site_url: \"https://test.example.com\",\n      health_status: \"unknown\" as const,\n      exchange_rate: 7.2,\n      account_info: {\n        access_token: \"sk-test-1234567890\",\n        username: \"test@example.com\",\n        quota: 100.0,\n        today_prompt_tokens: 0,\n        today_completion_tokens: 0,\n        today_quota_consumption: 0,\n        today_requests_count: 0\n      },\n      last_sync_time: Date.now()\n    };\n\n    console.log('1. 测试添加账号...');\n    const accountId = await accountStorage.addAccount(testAccount);\n    console.log('账号添加成功，ID:', accountId);\n\n    // 测试获取所有账号\n    console.log('2. 测试获取所有账号...');\n    const allAccounts = await accountStorage.getAllAccounts();\n    console.log('获取到账号数量:', allAccounts.length);\n    console.log('账号列表:', allAccounts.map(acc => ({\n      id: acc.id,\n      name: acc.site_name,\n      username: acc.account_info.username\n    })));\n\n    // 测试获取单个账号\n    console.log('3. 测试获取单个账号...');\n    const singleAccount = await accountStorage.getAccountById(accountId);\n    console.log('获取单个账号成功:', singleAccount ? singleAccount.site_name : '未找到');\n\n    // 测试数据导出\n    console.log('4. 测试数据导出...');\n    const exportedData = await accountStorage.exportData();\n    console.log('导出数据:', {\n      accountCount: exportedData.accounts.length,\n      lastUpdated: new Date(exportedData.last_updated).toLocaleString()\n    });\n\n    console.log('=== 所有测试完成 ===');\n    return true;\n  } catch (error) {\n    console.error('测试失败:', error);\n    return false;\n  }\n};\n\n// 在浏览器环境中暴露测试函数\nif (typeof window !== 'undefined') {\n  (window as any).testStorageFunction = testStorageFunction;\n  console.log('测试函数已挂载到 window.testStorageFunction，在控制台运行 testStorageFunction() 进行测试');\n}"
  },
  {
    "path": "docs/docs/.vuepress/config.js",
    "content": "import { defaultTheme } from '@vuepress/theme-default'\nimport { defineUserConfig } from 'vuepress'\nimport { viteBundler } from '@vuepress/bundler-vite'\n\nexport default defineUserConfig({\n  lang: 'zh-CN',\n  base: '/one-api-hub/',\n\n  title: 'One API Hub - 中转站管理器',\n  description: '一个开源的浏览器插件，聚合管理AI中转站账号的余额、模型和密钥，告别繁琐登录。',\n\n  theme: defaultTheme({\n    logo: 'https://github.com/fxaxg/one-api-hub/blob/main/assets/icon.png?raw=true',\n\n    navbar: ['/', '/get-started', '/faq'],\n  }),\n\n  bundler: viteBundler(),\n})\n"
  },
  {
    "path": "docs/docs/README.md",
    "content": "---\nhome: true\ntitle: 首页\nheroImage: https://github.com/fxaxg/one-api-hub/blob/main/assets/icon.png?raw=true\nheroText: One API Hub - 中转站管理器\ntagline: 一个开源的 AI 中转站账号管理插件\nactions:\n  - text: 开始使用\n    link: /get-started.html # 建议修改为您的实际文档路径，例如 /guide/\n    type: primary\n\n  - text: 查看源码\n    link: https://github.com/fxaxg/one-api-hub # 假设这是您的项目仓库地址\n    type: secondary\n\n  # ✨ 新增 Google Play 下载按钮\n  - text: Chrome 应用商店\n    link: https://chromewebstore.google.com/detail/%E4%B8%AD%E8%BD%AC%E7%AB%99%E7%AE%A1%E7%90%86%E5%99%A8-one-api-hub/eobdoeafpplhhhjfkinnlkljbkijpobd \n    type: secondary\n\nfeatures:\n  - title: 站点自动识别\n    details: 自动识别各类 AI 中转站点（如 One API, New API, Veloera），并自动创建系统访问 Token，添加到插件的站点列表中。\n  - title: 充值比例识别\n    details: 自动识别中转站的充值比例，让您清楚了解资金利用率。\n  - title: 多账号支持\n    details: 每个中转站点可添加和管理多个账号，满足您多账户需求。\n  - title: 余额与日志查询\n    details: 实时查看各账号的余额，并详细审查使用日志，掌握消费情况。\n  - title: 令牌(Key)管理\n    details: 便捷地查看与管理各站点的令牌(Key)，确保安全与效率。\n  - title: 模型与渠道信息\n    details: 详细展示站点支持的模型信息和所关联的渠道，助您做出最佳选择。\n  - title: 插件无需联网\n    details: 核心功能无需联网即可使用，保护您的数据隐私和使用稳定性。\n\nfooter: MIT Licensed | Copyright © 2023-present Ai API Hub\n---\n\n## 介绍\n\n目前市面上有太多 AI-API 中转站点，每次查看余额和支持模型列表等信息都非常麻烦，需要逐个登录查看。\n\n本插件致力于解决这一痛点，可以便捷地对基于 \n- [One-API]\n- [New-API] \n- [Veloera](https://github.com/Veloera/Veloera)\n等部署的 Ai 中转站账号进行整合管理，大大提升您的效率。\n\n---\n\n## 未来支持\n\n-   **模型降智测试**: 探索并评估不同模型在特定任务上的表现。\n-   **WebDAV 数据备份**: 支持将配置和数据备份到 WebDAV 服务，确保数据安全和可移植性。\n\n[One-API]: https://github.com/songquanpeng/one-api\n[New-API]: https://github.com/QuantumNous/new-api"
  },
  {
    "path": "docs/docs/faq.md",
    "content": "# 常见问题\n收集一些插件使用时遇到的常见问题。\n文档待完善，可先查看[使用教程](./get-started.md)"
  },
  {
    "path": "docs/docs/get-started.md",
    "content": "# 开始使用\n\n一个开源的浏览器插件，聚合管理AI中转站账号的余额、模型和密钥，告别繁琐登录。\n\n## 1. 下载\n\n::: info 推荐\n[前往 Chrome 应用商店]\n:::\n\n## 2. 支持的站点\n\n支持基于以下项目部署的中转站：\n - [One-API] \n - [New-API] \n - [Veloera](https://github.com/Veloera/Veloera)\n\n::: warning\n如果站点进行了二次开发导致一些关键接口（例如`/api/user`）发生了改变，则插件可能无法正常添加此站点。\n:::\n\n\n\n## 3. 添加站点\n::: info 提示\n必须先使用浏览器，自行在目标中转站登录，这样插件的自动识别功能才能通过cookie获取到您账号的[访问令牌(Access_Token)](#_3-2-手动添加)\n:::\n\n### 3.1 自动识别添加\n\n1. 打开插件主页面，点击`新增账号`\n\n![新增账号](./static/image/add-account-btn.png)\n\n2. 输入中转站地址，点击`自动识别`\n\n![自动识别](./static/image/add-account-dialog-btn.png)\n\n3. 确认自动识别无误后点击`确认添加`\n\n:::info 提示\n插件会自动识别您账号的：\n- 用户名\n- 用户ID\n- [访问令牌(Access_Token)](#_3-2-手动添加)\n- 充值金额比例\n:::\n\n![确认添加](./static/image/add-account-dialog-ok-btn.png)\n\n### 3.2 手动添加\n\n:::info 提示\n当自动识别未成功后，可进行手动输入添加站点账号，需要先获取以下信息。（每个站点可能UI有所差异，请自行寻找）\n:::\n![用户信息](./static/image/site-user-info.png)\n\n[One-API]: https://github.com/songquanpeng/one-api\n[New-API]: https://github.com/QuantumNous/new-api\n[前往 Chrome 应用商店]: https://chromewebstore.google.com/detail/%E4%B8%AD%E8%BD%AC%E7%AB%99%E7%AE%A1%E7%90%86%E5%99%A8-one-api-hub/eobdoeafpplhhhjfkinnlkljbkijpobd\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"one-api-hub-docs\",\n  \"description\": \"A VuePress project\",\n  \"version\": \"0.0.1\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@9.0.0\",\n  \"scripts\": {\n    \"docs:build\": \"vuepress build docs\",\n    \"docs:clean-dev\": \"vuepress dev docs --clean-cache\",\n    \"docs:dev\": \"vuepress dev docs\",\n    \"docs:update-package\": \"pnpm dlx vp-update\"\n  },\n  \"devDependencies\": {\n    \"@vuepress/bundler-vite\": \"2.0.0-rc.20\",\n    \"@vuepress/theme-default\": \"2.0.0-rc.88\",\n    \"sass-embedded\": \"^1.86.0\",\n    \"vue\": \"^3.5.13\",\n    \"vuepress\": \"2.0.0-rc.20\"\n  }\n}\n"
  },
  {
    "path": "examples/storageExample.ts",
    "content": "import { accountStorage, AccountStorageUtils } from \"../services/accountStorage\";\n\n/**\n * 账号存储系统使用示例\n */\nexport class AccountStorageExample {\n  /**\n   * 添加新账号示例\n   */\n  static async addNewAccount() {\n    try {\n      const newAccountData = {\n        emoji: \"\",\n        site_name: \"测试 API 站点\",\n        site_url: \"https://api.test.com\",\n        health_status: \"healthy\" as const,\n        exchange_rate: 7.2,\n        account_info: {\n          access_token: \"sk-test-xxxxxxxxxxxxxxxxxxxx\",\n          username: \"test_user@example.com\",\n          quota: 100.0,\n          today_prompt_tokens: 0,\n          today_completion_tokens: 0,\n          today_quota_consumption: 0,\n          today_requests_count: 0\n        },\n        last_sync_time: Date.now()\n      };\n\n      const accountId = await accountStorage.addAccount(newAccountData);\n      console.log(`新账号已添加，ID: ${accountId}`);\n      return accountId;\n    } catch (error) {\n      console.error('添加账号失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 更新账号统计信息示例\n   */\n  static async updateAccountStats(accountId: string) {\n    try {\n      const updates = {\n        account_info: {\n          access_token: \"保持原有token\",\n          username: \"保持原有用户名\",\n          quota: 95.50, // 更新余额\n          today_prompt_tokens: 1500,\n          today_completion_tokens: 2300,\n          today_quota_consumption: 4.50,\n          today_requests_count: 15\n        },\n        last_sync_time: Date.now()\n      };\n\n      const success = await accountStorage.updateAccount(accountId, updates);\n      console.log(`账号统计更新${success ? '成功' : '失败'}`);\n      return success;\n    } catch (error) {\n      console.error('更新账号统计失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 获取并显示所有账号信息示例\n   */\n  static async displayAllAccounts() {\n    try {\n      const accounts = await accountStorage.getAllAccounts();\n      console.log(`当前共有 ${accounts.length} 个账号:`);\n      \n      accounts.forEach((account, index) => {\n        console.log(`\\n${index + 1}. ${account.site_name}`);\n        console.log(`   用户名: ${account.account_info.username}`);\n        console.log(`   余额: ${AccountStorageUtils.formatBalance(account.account_info.quota, 'USD')}`);\n        console.log(`   今日消耗: ${AccountStorageUtils.formatBalance(account.account_info.today_quota_consumption, 'USD')}`);\n        console.log(`   今日请求: ${account.account_info.today_requests_count} 次`);\n        console.log(`   最后同步: ${new Date(account.last_sync_time).toLocaleString()}`);\n      });\n\n      return accounts;\n    } catch (error) {\n      console.error('获取账号信息失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 获取统计信息示例\n   */\n  static async displayStats() {\n    try {\n      const stats = await accountStorage.getAccountStats();\n      \n      console.log('\\n=== 总体统计 ===');\n      console.log(`总余额: ${AccountStorageUtils.formatBalance(stats.total_quota, 'USD')}`);\n      console.log(`今日总消耗: ${AccountStorageUtils.formatBalance(stats.today_total_consumption, 'USD')}`);\n      console.log(`今日总请求: ${stats.today_total_requests} 次`);\n      console.log(`今日 Prompt Tokens: ${AccountStorageUtils.formatTokenCount(stats.today_total_prompt_tokens)}`);\n      console.log(`今日 Completion Tokens: ${AccountStorageUtils.formatTokenCount(stats.today_total_completion_tokens)}`);\n\n      return stats;\n    } catch (error) {\n      console.error('获取统计信息失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 数据转换示例（兼容现有 UI）\n   */\n  static async convertForUI() {\n    try {\n      const accounts = await accountStorage.getAllAccounts();\n      const displayData = accountStorage.convertToDisplayData(accounts);\n      \n      console.log('\\n=== 转换为 UI 数据格式 ===');\n      console.log(JSON.stringify(displayData, null, 2));\n\n      return displayData;\n    } catch (error) {\n      console.error('数据转换失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 数据导出/导入示例\n   */\n  static async exportImportExample() {\n    try {\n      // 导出数据\n      const exportedData = await accountStorage.exportData();\n      console.log('数据已导出');\n      \n      // 模拟导入相同数据\n      const importSuccess = await accountStorage.importData(exportedData);\n      console.log(`数据导入${importSuccess ? '成功' : '失败'}`);\n\n      return { exportedData, importSuccess };\n    } catch (error) {\n      console.error('导出/导入失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 数据验证示例\n   */\n  static validateAccountData() {\n    const validAccount = {\n      emoji: \"\",\n      site_name: \"有效站点\",\n      site_url: \"https://api.valid.com\",\n      health_status: \"healthy\" as const,\n      exchange_rate: 7.2,\n      account_info: {\n        access_token: \"sk-valid-token\",\n        username: \"valid_user\",\n        quota: 100,\n        today_prompt_tokens: 0,\n        today_completion_tokens: 0,\n        today_quota_consumption: 0,\n        today_requests_count: 0\n      }\n    };\n\n    const invalidAccount = {\n      emoji: \"\",\n      site_name: \"\", // 空的站点名称\n      site_url: \"invalid-url\", // 不是有效URL\n      health_status: undefined as any, // 缺少健康状态\n      exchange_rate: -1, // 无效的充值比例\n      account_info: {\n        access_token: \"\", // 空的token\n        username: \"\",\n        quota: 100,\n        today_prompt_tokens: 0,\n        today_completion_tokens: 0,\n        today_quota_consumption: 0,\n        today_requests_count: 0\n      }\n    };\n\n    console.log('\\n=== 数据验证示例 ===');\n    console.log('有效账号验证结果:', AccountStorageUtils.validateAccount(validAccount));\n    console.log('无效账号验证结果:', AccountStorageUtils.validateAccount(invalidAccount));\n  }\n\n  /**\n   * 运行所有示例\n   */\n  static async runAllExamples() {\n    console.log('=== 账号存储系统示例 ===\\n');\n    \n    try {\n      // 数据验证示例\n      this.validateAccountData();\n      \n      // 添加账号\n      const accountId = await this.addNewAccount();\n      \n      // 显示所有账号\n      await this.displayAllAccounts();\n      \n      // 更新账号统计\n      await this.updateAccountStats(accountId);\n      \n      // 显示统计信息\n      await this.displayStats();\n      \n      // 数据转换示例\n      await this.convertForUI();\n      \n      // 导出/导入示例\n      await this.exportImportExample();\n      \n      console.log('\\n=== 所有示例运行完成 ===');\n    } catch (error) {\n      console.error('示例运行失败:', error);\n    }\n  }\n}\n\n// 如果直接运行此文件，执行所有示例\nif (typeof window !== 'undefined') {\n  // 浏览器环境\n  (window as any).AccountStorageExample = AccountStorageExample;\n}"
  },
  {
    "path": "global.d.ts",
    "content": "declare module \"*.png\" {\n  const content: string;\n  export default content;\n}\n\ndeclare module \"*.jpg\" {\n  const content: string;\n  export default content;\n}\n\ndeclare module \"*.jpeg\" {\n  const content: string;\n  export default content;\n}\n\ndeclare module \"*.gif\" {\n  const content: string;\n  export default content;\n}\n\ndeclare module \"*.svg\" {\n  const content: string;\n  export default content;\n}\n\ndeclare module \"*.webp\" {\n  const content: string;\n  export default content;\n}"
  },
  {
    "path": "hooks/useAccountData.ts",
    "content": "import { useState, useEffect, useCallback } from \"react\"\nimport { accountStorage } from \"../services/accountStorage\"\nimport type { SiteAccount, AccountStats, DisplaySiteData } from \"../types\"\n\ninterface UseAccountDataResult {\n  // 数据状态\n  accounts: SiteAccount[]\n  displayData: DisplaySiteData[]\n  stats: AccountStats\n  lastUpdateTime: Date\n  \n  // 加载状态\n  isInitialLoad: boolean\n  isRefreshing: boolean\n  \n  // 动画相关状态\n  prevTotalConsumption: { USD: number; CNY: number }\n  prevBalances: { [id: string]: { USD: number; CNY: number } }\n  \n  // 操作函数\n  loadAccountData: () => Promise<void>\n  handleRefresh: () => Promise<{ success: number; failed: number }>\n}\n\nexport const useAccountData = (): UseAccountDataResult => {\n  // 数据状态\n  const [accounts, setAccounts] = useState<SiteAccount[]>([])\n  const [displayData, setDisplayData] = useState<DisplaySiteData[]>([])\n  const [stats, setStats] = useState<AccountStats>({\n    total_quota: 0,\n    today_total_consumption: 0,\n    today_total_requests: 0,\n    today_total_prompt_tokens: 0,\n    today_total_completion_tokens: 0\n  })\n  const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date())\n  \n  // 加载状态\n  const [isInitialLoad, setIsInitialLoad] = useState(true)\n  const [isRefreshing, setIsRefreshing] = useState(false)\n  \n  // 动画相关状态\n  const [prevTotalConsumption, setPrevTotalConsumption] = useState({ USD: 0, CNY: 0 })\n  const [prevBalances, setPrevBalances] = useState<{ [id: string]: { USD: number, CNY: number } }>({})\n\n  // 加载账号数据\n  const loadAccountData = useCallback(async () => {\n    try {\n      const allAccounts = await accountStorage.getAllAccounts()\n      const accountStats = await accountStorage.getAccountStats()\n      const displaySiteData = accountStorage.convertToDisplayData(allAccounts)\n\n      // 计算新的余额数据\n      const newBalances: { [id: string]: { USD: number, CNY: number } } = {}\n      displaySiteData.forEach(site => {\n        newBalances[site.id] = {\n          USD: site.balance.USD,\n          CNY: site.balance.CNY\n        }\n      })\n\n      // 如果不是初始加载，保存之前的数值供动画使用\n      if (!isInitialLoad) {\n        setPrevTotalConsumption(prevTotalConsumption)\n        setPrevBalances(prevBalances)\n      }\n\n      // 更新状态\n      setAccounts(allAccounts)\n      setStats(accountStats)\n      setDisplayData(displaySiteData)\n      \n      // 更新最后同步时间为最近的一次同步时间\n      if (allAccounts.length > 0) {\n        const latestSyncTime = Math.max(...allAccounts.map(acc => acc.last_sync_time))\n        if (latestSyncTime > 0) {\n          setLastUpdateTime(new Date(latestSyncTime))\n        }\n      }\n\n      // 标记为非初始加载\n      if (isInitialLoad) {\n        setIsInitialLoad(false)\n      }\n      \n      console.log('账号数据加载完成:', { \n        accountCount: allAccounts.length, \n        stats: accountStats \n      })\n    } catch (error) {\n      console.error('加载账号数据失败:', error)\n    }\n  }, [isInitialLoad, prevTotalConsumption, prevBalances])\n\n  // 刷新数据\n  const handleRefresh = useCallback(async () => {\n    setIsRefreshing(true)\n    try {\n      // 刷新所有账号数据\n      const refreshResult = await accountStorage.refreshAllAccounts()\n      console.log('刷新结果:', refreshResult)\n      \n      // 重新加载显示数据\n      await loadAccountData()\n      setLastUpdateTime(new Date())\n      \n      // 返回刷新结果，让组件层处理 UI 反馈\n      return refreshResult\n    } catch (error) {\n      console.error('刷新数据失败:', error)\n      // 即使刷新失败也尝试加载本地数据\n      await loadAccountData()\n      throw error\n    } finally {\n      setIsRefreshing(false)\n    }\n  }, [loadAccountData])\n\n  // 组件初始化时加载数据\n  useEffect(() => {\n    loadAccountData()\n  }, [loadAccountData])\n\n  return {\n    // 数据状态\n    accounts,\n    displayData,\n    stats,\n    lastUpdateTime,\n    \n    // 加载状态\n    isInitialLoad,\n    isRefreshing,\n    \n    // 动画相关状态\n    prevTotalConsumption,\n    prevBalances,\n    \n    // 操作函数\n    loadAccountData,\n    handleRefresh\n  }\n}"
  },
  {
    "path": "hooks/useSort.ts",
    "content": "import { useState, useMemo, useCallback, useEffect } from \"react\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport { createSortComparator } from \"../utils/formatters\"\nimport type { DisplaySiteData } from \"../types\"\n\ntype SortField = 'name' | 'balance' | 'consumption'\ntype SortOrder = 'asc' | 'desc'\n\ninterface UseSortResult {\n  sortField: SortField\n  sortOrder: SortOrder\n  sortedData: DisplaySiteData[]\n  handleSort: (field: SortField) => void\n}\n\nexport const useSort = (\n  data: DisplaySiteData[],\n  currencyType: 'USD' | 'CNY',\n  initialSortField?: SortField,\n  initialSortOrder?: SortOrder,\n  onSortChange?: (field: SortField, order: SortOrder) => void\n): UseSortResult => {\n  const [sortField, setSortField] = useState<SortField>(\n    initialSortField || UI_CONSTANTS.SORT.DEFAULT_FIELD\n  )\n  const [sortOrder, setSortOrder] = useState<SortOrder>(\n    initialSortOrder || UI_CONSTANTS.SORT.DEFAULT_ORDER\n  )\n\n  // 当初始值变化时更新状态\n  useEffect(() => {\n    if (initialSortField !== undefined) {\n      setSortField(initialSortField)\n    }\n  }, [initialSortField])\n\n  useEffect(() => {\n    if (initialSortOrder !== undefined) {\n      setSortOrder(initialSortOrder)\n    }\n  }, [initialSortOrder])\n\n  // 处理排序\n  const handleSort = useCallback((field: SortField) => {\n    let newOrder: SortOrder\n    \n    if (sortField === field) {\n      newOrder = sortOrder === 'asc' ? 'desc' : 'asc'\n      setSortOrder(newOrder)\n    } else {\n      newOrder = 'asc'\n      setSortField(field)  \n      setSortOrder(newOrder)\n    }\n    \n    // 通知父组件排序变化\n    onSortChange?.(field === sortField ? sortField : field, newOrder)\n  }, [sortField, sortOrder, onSortChange])\n\n  // 排序数据\n  const sortedData = useMemo(() => {\n    return [...data].sort((a, b) => {\n      let aValue: string | number, bValue: string | number\n      \n      switch (sortField) {\n        case 'name':\n          aValue = a.name\n          bValue = b.name\n          break\n        case 'balance':\n          aValue = a.balance[currencyType]\n          bValue = b.balance[currencyType]\n          break\n        case 'consumption':\n          aValue = a.todayConsumption[currencyType]\n          bValue = b.todayConsumption[currencyType]\n          break\n        default:\n          return 0\n      }\n      \n      if (sortOrder === 'asc') {\n        return aValue < bValue ? -1 : aValue > bValue ? 1 : 0\n      } else {\n        return aValue > bValue ? -1 : aValue < bValue ? 1 : 0\n      }\n    })\n  }, [data, sortField, sortOrder, currencyType])\n\n  return {\n    sortField,\n    sortOrder,\n    sortedData,\n    handleSort\n  }\n}"
  },
  {
    "path": "hooks/useTimeFormatter.ts",
    "content": "import { useState, useEffect } from \"react\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport { formatRelativeTime, formatFullTime } from \"../utils/formatters\"\n\ninterface UseTimeFormatterResult {\n  formatRelativeTime: (date: Date) => string\n  formatFullTime: (date: Date) => string\n  forceUpdate: () => void\n}\n\nexport const useTimeFormatter = (): UseTimeFormatterResult => {\n  const [, setForceUpdate] = useState({})\n\n  // 强制更新函数\n  const forceUpdate = () => {\n    setForceUpdate({})\n  }\n\n  // 定时更新相对时间显示\n  useEffect(() => {\n    const updateInterval = setInterval(() => {\n      forceUpdate()\n    }, UI_CONSTANTS.UPDATE_INTERVAL)\n\n    return () => clearInterval(updateInterval)\n  }, [])\n\n  return {\n    formatRelativeTime,\n    formatFullTime,\n    forceUpdate\n  }\n}"
  },
  {
    "path": "hooks/useUserPreferences.ts",
    "content": "import { useState, useEffect, useCallback } from 'react';\nimport { userPreferences, type UserPreferences } from '../services/userPreferences';\n\n/**\n * 用户偏好设置管理Hook\n */\nexport function useUserPreferences() {\n  const [preferences, setPreferences] = useState<UserPreferences | null>(null);\n  const [isLoading, setIsLoading] = useState(true);\n\n  // 加载偏好设置\n  const loadPreferences = useCallback(async () => {\n    try {\n      setIsLoading(true);\n      const prefs = await userPreferences.getPreferences();\n      setPreferences(prefs);\n      console.log('[useUserPreferences] 偏好设置加载成功:', prefs);\n    } catch (error) {\n      console.error('[useUserPreferences] 加载偏好设置失败:', error);\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  // 初始化加载\n  useEffect(() => {\n    loadPreferences();\n  }, [loadPreferences]);\n\n  // 更新活动标签页\n  const updateActiveTab = useCallback(async (activeTab: 'consumption' | 'balance') => {\n    try {\n      const success = await userPreferences.updateActiveTab(activeTab);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, activeTab } : null);\n        console.log('[useUserPreferences] 活动标签页更新成功:', activeTab);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新活动标签页失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 更新货币类型\n  const updateCurrencyType = useCallback(async (currencyType: 'USD' | 'CNY') => {\n    try {\n      const success = await userPreferences.updateCurrencyType(currencyType);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, currencyType } : null);\n        console.log('[useUserPreferences] 货币类型更新成功:', currencyType);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新货币类型失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 更新排序配置\n  const updateSortConfig = useCallback(async (sortField: 'name' | 'balance' | 'consumption', sortOrder: 'asc' | 'desc') => {\n    try {\n      const success = await userPreferences.updateSortConfig(sortField, sortOrder);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, sortField, sortOrder } : null);\n        console.log('[useUserPreferences] 排序配置更新成功:', { sortField, sortOrder });\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新排序配置失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 更新自动刷新设置\n  const updateAutoRefresh = useCallback(async (autoRefresh: boolean) => {\n    try {\n      const success = await userPreferences.updateAutoRefresh(autoRefresh);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, autoRefresh } : null);\n        console.log('[useUserPreferences] 自动刷新设置更新成功:', autoRefresh);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新自动刷新设置失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 更新刷新间隔\n  const updateRefreshInterval = useCallback(async (refreshInterval: number) => {\n    try {\n      const success = await userPreferences.updateRefreshInterval(refreshInterval);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, refreshInterval } : null);\n        console.log('[useUserPreferences] 刷新间隔更新成功:', refreshInterval);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新刷新间隔失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 更新打开插件时自动刷新设置\n  const updateRefreshOnOpen = useCallback(async (refreshOnOpen: boolean) => {\n    try {\n      const success = await userPreferences.updateRefreshOnOpen(refreshOnOpen);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, refreshOnOpen } : null);\n        console.log('[useUserPreferences] 打开插件时自动刷新设置更新成功:', refreshOnOpen);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新打开插件时自动刷新设置失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 更新健康状态显示设置\n  const updateShowHealthStatus = useCallback(async (showHealthStatus: boolean) => {\n    try {\n      const success = await userPreferences.updateShowHealthStatus(showHealthStatus);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, showHealthStatus } : null);\n        console.log('[useUserPreferences] 健康状态显示设置更新成功:', showHealthStatus);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 更新健康状态显示设置失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 批量更新偏好设置\n  const updatePreferences = useCallback(async (updates: Partial<UserPreferences>) => {\n    try {\n      const success = await userPreferences.savePreferences(updates);\n      if (success && preferences) {\n        setPreferences(prev => prev ? { ...prev, ...updates } : null);\n        console.log('[useUserPreferences] 偏好设置批量更新成功:', updates);\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 批量更新偏好设置失败:', error);\n      return false;\n    }\n  }, [preferences]);\n\n  // 重置为默认设置\n  const resetToDefaults = useCallback(async () => {\n    try {\n      const success = await userPreferences.resetToDefaults();\n      if (success) {\n        await loadPreferences(); // 重新加载设置\n        console.log('[useUserPreferences] 已重置为默认设置');\n      }\n      return success;\n    } catch (error) {\n      console.error('[useUserPreferences] 重置设置失败:', error);\n      return false;\n    }\n  }, [loadPreferences]);\n\n  return {\n    // 状态\n    preferences,\n    isLoading,\n\n    // 便捷访问属性\n    activeTab: preferences?.activeTab || 'consumption',\n    currencyType: preferences?.currencyType || 'USD',\n    sortField: preferences?.sortField || 'name',\n    sortOrder: preferences?.sortOrder || 'asc',\n    autoRefresh: preferences?.autoRefresh ?? true,\n    refreshInterval: preferences?.refreshInterval ?? 360,\n    refreshOnOpen: preferences?.refreshOnOpen ?? true,\n    showHealthStatus: preferences?.showHealthStatus ?? true,\n\n    // 操作方法\n    updateActiveTab,\n    updateCurrencyType,\n    updateSortConfig,\n    updateAutoRefresh,\n    updateRefreshInterval,\n    updateRefreshOnOpen,\n    updateShowHealthStatus,\n    updatePreferences,\n    resetToDefaults,\n    loadPreferences\n  };\n}"
  },
  {
    "path": "options/index.tsx",
    "content": "import \"../popup/style.css\"\nimport { useState, useEffect } from \"react\"\nimport { \n  CogIcon,\n  CpuChipIcon,\n  KeyIcon,\n  ArrowPathIcon,\n  InformationCircleIcon\n} from \"@heroicons/react/24/outline\"\nimport { Toaster } from 'react-hot-toast'\nimport iconImage from \"../assets/icon.png\"\n\n// 页面组件导入\nimport BasicSettings from \"./pages/BasicSettings\"\nimport ModelList from \"./pages/ModelList\"\nimport KeyManagement from \"./pages/KeyManagement\"\nimport ImportExport from \"./pages/ImportExport\"\nimport About from \"./pages/About\"\n\n// 菜单项类型定义\ninterface MenuItem {\n  id: string\n  name: string\n  icon: React.ComponentType<{ className?: string }>\n  component: React.ComponentType<any>\n}\n\n// 菜单配置\nconst menuItems: MenuItem[] = [\n  {\n    id: 'basic',\n    name: '基本设置',\n    icon: CogIcon,\n    component: BasicSettings\n  },\n  {\n    id: 'models',\n    name: '模型列表',\n    icon: CpuChipIcon,\n    component: ModelList\n  },\n  {\n    id: 'keys',\n    name: '密钥管理',\n    icon: KeyIcon,\n    component: KeyManagement\n  },\n  {\n    id: 'import-export',\n    name: '导入/导出',\n    icon: ArrowPathIcon,\n    component: ImportExport\n  },\n  {\n    id: 'about',\n    name: '关于',\n    icon: InformationCircleIcon,\n    component: About\n  }\n]\n\n// 解析URL hash和参数\nfunction parseHash() {\n  const hash = window.location.hash.slice(1) // 去掉 #\n  if (!hash) return { page: 'basic', params: {} }\n  \n  const [page, ...paramParts] = hash.split('?')\n  const params: Record<string, string> = {}\n  \n  if (paramParts.length > 0) {\n    const paramString = paramParts.join('?')\n    const urlParams = new URLSearchParams(paramString)\n    for (const [key, value] of urlParams.entries()) {\n      params[key] = value\n    }\n  }\n  \n  return { page: page || 'basic', params }\n}\n\n// 更新URL hash\nfunction updateHash(page: string, params?: Record<string, string>) {\n  let hash = `#${page}`\n  if (params && Object.keys(params).length > 0) {\n    const searchParams = new URLSearchParams(params)\n    hash += `?${searchParams.toString()}`\n  }\n  window.history.replaceState(null, '', hash)\n}\n\nfunction OptionsPage() {\n  const [activeMenuItem, setActiveMenuItem] = useState('basic')\n  const [routeParams, setRouteParams] = useState<Record<string, string>>({})\n\n  // 初始化路由\n  useEffect(() => {\n    const { page, params } = parseHash()\n    const validPage = menuItems.find(item => item.id === page) ? page : 'basic'\n    setActiveMenuItem(validPage)\n    setRouteParams(params)\n    \n    // 监听浏览器前进后退\n    const handleHashChange = () => {\n      const { page, params } = parseHash()\n      const validPage = menuItems.find(item => item.id === page) ? page : 'basic'\n      setActiveMenuItem(validPage)\n      setRouteParams(params)\n    }\n    \n    window.addEventListener('hashchange', handleHashChange)\n    return () => window.removeEventListener('hashchange', handleHashChange)\n  }, [])\n\n  // 切换菜单项\n  const handleMenuItemChange = (itemId: string, params?: Record<string, string>) => {\n    setActiveMenuItem(itemId)\n    setRouteParams(params || {})\n    updateHash(itemId, params)\n  }\n\n  // 获取当前活动的组件\n  const ActiveComponent = menuItems.find(item => item.id === activeMenuItem)?.component || BasicSettings\n\n  return (\n    <div className=\"min-h-screen bg-gray-50\">\n      {/* 顶部导航栏 */}\n      <header className=\"bg-white shadow-sm border-b border-gray-200\">\n        <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n          <div className=\"flex items-center h-16\">\n            {/* 插件图标和名称 */}\n            <div className=\"flex items-center space-x-3\">\n              <img \n                src={iconImage} \n                alt=\"One API Hub\" \n                className=\"w-8 h-8 rounded-lg shadow-sm\"\n              />\n              <div>\n                <h1 className=\"text-xl font-semibold text-gray-900\">One API Hub</h1>\n                <p className=\"text-sm text-gray-500\">AI 中转站账号管理插件</p>\n              </div>\n            </div>\n          </div>\n        </div>\n      </header>\n\n      <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n        <div className=\"flex gap-8\">\n          {/* 左侧菜单导航栏 */}\n          <aside className=\"w-64 flex-shrink-0\">\n            <nav className=\"bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden\">\n              <div className=\"p-4 border-b border-gray-100\">\n                <h2 className=\"text-sm font-medium text-gray-500 uppercase tracking-wide\">设置选项</h2>\n              </div>\n              <ul className=\"divide-y divide-gray-100\">\n                {menuItems.map((item) => {\n                  const Icon = item.icon\n                  const isActive = activeMenuItem === item.id\n                  \n                  return (\n                    <li key={item.id}>\n                      <button\n                        onClick={() => handleMenuItemChange(item.id)}\n                        className={`w-full flex items-center px-4 py-3 text-left hover:bg-gray-50 transition-colors ${\n                          isActive \n                            ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600' \n                            : 'text-gray-700'\n                        }`}\n                      >\n                        <Icon className={`w-5 h-5 mr-3 ${\n                          isActive ? 'text-blue-600' : 'text-gray-400'\n                        }`} />\n                        <span className=\"font-medium\">{item.name}</span>\n                      </button>\n                    </li>\n                  )\n                })}\n              </ul>\n            </nav>\n          </aside>\n\n          {/* 右侧内容区域 */}\n          <main className=\"flex-1 min-w-0\">\n            <div className=\"bg-white rounded-lg shadow-sm border border-gray-200 min-h-[600px]\">\n              <ActiveComponent routeParams={routeParams} />\n            </div>\n          </main>\n        </div>\n      </div>\n      <Toaster\n        position=\"bottom-center\"\n        reverseOrder={false}\n        gutter={8}\n        containerClassName=\"\"\n        containerStyle={{}}\n        toastOptions={{\n          className: '',\n          duration: 4000,\n          style: {\n            background: '#fff',\n            color: '#363636',\n          },\n          success: {\n            duration: 3000,\n          },\n          error: {\n            duration: 5000,\n          },\n        }}\n      />\n    </div>\n  )\n}\n\nexport default OptionsPage"
  },
  {
    "path": "options/pages/About.tsx",
    "content": "import { InformationCircleIcon, HeartIcon, GlobeAltIcon, CodeBracketIcon } from \"@heroicons/react/24/outline\"\nimport iconImage from \"../../assets/icon.png\"\nimport packageJson from \"../../package.json\"\n\nexport default function About() {\n  const version = packageJson.version\n\n  const features = [\n    \"自动识别中转站点，自动创建系统访问 token\",\n    \"每个站点可添加多个账号\",\n    \"账号的余额、使用日志进行查看\",\n    \"密钥(key)查看与管理\",\n    \"站点支持模型信息和渠道查看\",\n    \"插件无需联网，保护隐私安全\"\n  ]\n\n  const futureFeatures = [\n    \"模型降智测试\",\n    \"WebDAV 数据备份\",\n    \"更多API站点支持\",\n    \"高级统计分析功能\"\n  ]\n\n  const techStack = [\n    { name: \"Plasmo\", version: \"0.90.5\", description: \"浏览器扩展开发框架\" },\n    { name: \"React\", version: \"18.2.0\", description: \"用户界面库\" },\n    { name: \"TypeScript\", version: \"5.3.3\", description: \"类型安全的JavaScript\" },\n    { name: \"Tailwind CSS\", version: \"3.4.17\", description: \"原子化CSS框架\" },\n    { name: \"Headless UI\", version: \"2.2.4\", description: \"无样式UI组件\" }\n  ]\n\n  return (\n    <div className=\"p-6\">\n      {/* 页面标题 */}\n      <div className=\"mb-8\">\n        <div className=\"flex items-center space-x-3 mb-2\">\n          <InformationCircleIcon className=\"w-6 h-6 text-blue-600\" />\n          <h1 className=\"text-2xl font-semibold text-gray-900\">关于</h1>\n        </div>\n        <p className=\"text-gray-500\">了解插件信息和开发团队</p>\n      </div>\n\n      <div className=\"space-y-8\">\n        {/* 插件信息 */}\n        <section>\n          <div className=\"bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-6\">\n            <div className=\"flex items-start space-x-4\">\n              <img \n                src={iconImage} \n                alt=\"One API Hub\" \n                className=\"w-16 h-16 rounded-lg shadow-sm flex-shrink-0\"\n              />\n              <div className=\"flex-1\">\n                <h2 className=\"text-2xl font-bold text-gray-900 mb-2\">One API Hub</h2>\n                <p className=\"text-gray-600 mb-4\">\n                  AI 中转站账号管理插件，帮助用户便捷地管理多个AI API中转站点的账号。\n                </p>\n                <div className=\"text-sm\">\n                  <div>\n                    <span className=\"text-gray-500\">版本号:</span>\n                    <span className=\"ml-2 font-medium text-gray-900\">v{version}</span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        {/* 项目链接 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-gray-900 mb-4\">项目链接</h2>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n            <div className=\"bg-white border border-gray-200 rounded-lg p-6\">\n              <div className=\"flex items-start space-x-4\">\n                <CodeBracketIcon className=\"w-6 h-6 text-gray-900 mt-1 flex-shrink-0\" />\n                <div className=\"flex-1\">\n                  <h3 className=\"font-medium text-gray-900 mb-2\">GitHub 仓库</h3>\n                  <p className=\"text-sm text-gray-600 mb-3\">\n                    查看源代码、提交问题或参与项目开发\n                  </p>\n                  <a \n                    href=\"https://github.com/fxaxg/one-api-hub\" \n                    target=\"_blank\" \n                    rel=\"noopener noreferrer\"\n                    className=\"inline-flex items-center px-3 py-1.5 rounded-md text-sm font-medium bg-gray-900 text-white hover:bg-gray-800 transition-colors\"\n                  >\n                    去点个Star\n                  </a>\n                </div>\n              </div>\n            </div>\n            <div className=\"bg-white border border-gray-200 rounded-lg p-6\">\n              <div className=\"flex items-start space-x-4\">\n                <GlobeAltIcon className=\"w-6 h-6 text-blue-600 mt-1 flex-shrink-0\" />\n                <div className=\"flex-1\">\n                  <h3 className=\"font-medium text-gray-900 mb-2\">项目官网</h3>\n                  <p className=\"text-sm text-gray-600 mb-3\">\n                    查看详细文档、使用指南和更多信息\n                  </p>\n                  <a \n                    href=\"https://fxaxg.github.io/one-api-hub/\" \n                    target=\"_blank\" \n                    rel=\"noopener noreferrer\"\n                    className=\"inline-flex items-center px-3 py-1.5 rounded-md text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 transition-colors\"\n                  >\n                    访问官网\n                  </a>\n                </div>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        {/* 功能特性 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-gray-900 mb-4\">功能特性</h2>\n          <div className=\"space-y-6\">\n            {/* 主要功能 */}\n            <div>\n              <h3 className=\"text-base font-medium text-gray-800 mb-3 flex items-center\">\n                <div className=\"w-2 h-2 bg-green-500 rounded-full mr-2\"></div>\n                已实现功能\n              </h3>\n              <div className=\"bg-green-50 border border-green-200 rounded-lg p-4\">\n                <ul className=\"space-y-2\">\n                  {features.map((feature, index) => (\n                    <li key={index} className=\"flex items-start space-x-2 text-sm text-green-800\">\n                      <div className=\"w-1.5 h-1.5 bg-green-500 rounded-full mt-2 flex-shrink-0\"></div>\n                      <span>{feature}</span>\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            </div>\n            \n            {/* 未来功能 */}\n            <div>\n              <h3 className=\"text-base font-medium text-gray-800 mb-3 flex items-center\">\n                <div className=\"w-2 h-2 bg-blue-500 rounded-full mr-2\"></div>\n                即将支持\n              </h3>\n              <div className=\"bg-blue-50 border border-blue-200 rounded-lg p-4\">\n                <ul className=\"space-y-2\">\n                  {futureFeatures.map((feature, index) => (\n                    <li key={index} className=\"flex items-start space-x-2 text-sm text-blue-800\">\n                      <div className=\"w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 flex-shrink-0\"></div>\n                      <span>{feature}</span>\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        {/* 技术栈 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-gray-900 mb-4\">技术栈</h2>\n          <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n            {techStack.map((tech, index) => (\n              <div key={index} className=\"bg-white border border-gray-200 rounded-lg p-4\">\n                <div className=\"flex items-center justify-between mb-2\">\n                  <h3 className=\"font-medium text-gray-900\">{tech.name}</h3>\n                  <span className=\"text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded\">\n                    v{tech.version}\n                  </span>\n                </div>\n                <p className=\"text-sm text-gray-500\">{tech.description}</p>\n              </div>\n            ))}\n          </div>\n        </section>\n\n\n        {/* 版权和致谢 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-gray-900 mb-4\">版权与致谢</h2>\n          <div className=\"bg-white border border-gray-200 rounded-lg p-6\">\n            <div className=\"flex items-start space-x-4\">\n              <HeartIcon className=\"w-6 h-6 text-red-500 mt-1 flex-shrink-0\" />\n              <div className=\"flex-1\">\n                <h3 className=\"font-medium text-gray-900 mb-2\">开发与维护</h3>\n                <p className=\"text-sm text-gray-600 mb-4\">\n                  感谢所有为开源社区做出贡献的开发者们，本插件的开发得益于这些优秀的开源项目和工具。\n                </p>\n                <div className=\"flex flex-wrap gap-2\">\n                  <span className=\"inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800\">\n                    Made with ❤️\n                  </span>\n                  <span className=\"inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800\">\n                    Open Source\n                  </span>\n                  <span className=\"inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800\">\n                    Privacy First\n                  </span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        {/* 隐私声明 */}\n        <section>\n          <div className=\"bg-green-50 border border-green-200 rounded-lg p-4\">\n            <div className=\"flex items-start space-x-3\">\n              <InformationCircleIcon className=\"w-5 h-5 text-green-600 mt-0.5 flex-shrink-0\" />\n              <div className=\"text-sm\">\n                <p className=\"text-green-800 font-medium mb-1\">隐私保护</p>\n                <p className=\"text-green-700\">\n                  本插件所有数据均存储在本地浏览器中，不会上传到任何服务器。\n                  您的账号信息和使用数据完全由您自己掌控，确保隐私安全。\n                </p>\n              </div>\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "options/pages/BasicSettings.tsx",
    "content": "import { useState, useEffect } from \"react\"\nimport { Switch } from \"@headlessui/react\"\nimport { CogIcon, GlobeAltIcon, EyeIcon, ArrowPathIcon } from \"@heroicons/react/24/outline\"\nimport { useUserPreferences } from \"../../hooks/useUserPreferences\"\nimport toast from 'react-hot-toast'\n\nexport default function BasicSettings() {\n  const {\n    preferences,\n    isLoading,\n    currencyType,\n    activeTab,\n    updateCurrencyType,\n    updateActiveTab,\n    updateAutoRefresh,\n    updateRefreshInterval,\n    updateRefreshOnOpen,\n    resetToDefaults\n  } = useUserPreferences()\n\n  // 从偏好设置中获取值，或使用默认值\n  const autoRefresh = preferences?.autoRefresh ?? true\n  const refreshInterval = preferences?.refreshInterval ?? 360\n  const refreshOnOpen = preferences?.refreshOnOpen ?? true\n\n  // 本地状态用于输入框编辑\n  const [intervalInput, setIntervalInput] = useState<string>(refreshInterval.toString())\n\n  // 同步刷新间隔值到输入框\n  useEffect(() => {\n    setIntervalInput(refreshInterval.toString())\n  }, [refreshInterval])\n\n  const handleCurrencyChange = async (currency: 'USD' | 'CNY') => {\n    const success = await updateCurrencyType(currency)\n    if (success) {\n      toast.success(`货币单位已切换到 ${currency === 'USD' ? '美元' : '人民币'}`)\n    } else {\n      toast.error('设置保存失败')\n    }\n  }\n\n  const handleDefaultTabChange = async (tab: 'consumption' | 'balance') => {\n    const success = await updateActiveTab(tab)\n    if (success) {\n      toast.success(`默认标签页已设置为 ${tab === 'consumption' ? '今日消耗' : '总余额'}`)\n    } else {\n      toast.error('设置保存失败')\n    }\n  }\n\n  const handleAutoRefreshChange = async (enabled: boolean) => {\n    const success = await updateAutoRefresh(enabled)\n    if (success) {\n      // 通知后台更新设置\n      chrome.runtime.sendMessage({\n        action: 'updateAutoRefreshSettings',\n        settings: { autoRefresh: enabled }\n      });\n      toast.success(`自动刷新已${enabled ? '启用' : '关闭'}`)\n    } else {\n      toast.error('设置保存失败')\n    }\n  }\n\n  const handleRefreshIntervalChange = async (value: string) => {\n    // 直接更新输入框状态，允许用户清空和编辑\n    setIntervalInput(value)\n  }\n\n  const handleRefreshIntervalBlur = async () => {\n    const interval = Number(intervalInput)\n    \n    // 验证输入值\n    if (!intervalInput || isNaN(interval) || interval < 10) {\n      toast.error('刷新间隔必须大于等于10秒')\n      setIntervalInput(refreshInterval.toString()) // 恢复原值\n      return\n    }\n\n    // 保存设置\n    const success = await updateRefreshInterval(interval)\n    if (success) {\n      // 通知后台更新设置\n      chrome.runtime.sendMessage({\n        action: 'updateAutoRefreshSettings',\n        settings: { refreshInterval: interval }\n      });\n      toast.success(`刷新间隔已设置为 ${interval} 秒`)\n    } else {\n      toast.error('设置保存失败')\n      setIntervalInput(refreshInterval.toString()) // 恢复原值\n    }\n  }\n\n  const handleRefreshOnOpenChange = async (enabled: boolean) => {\n    const success = await updateRefreshOnOpen(enabled)\n    if (success) {\n      toast.success(`打开插件时自动刷新已${enabled ? '启用' : '关闭'}`)\n    } else {\n      toast.error('设置保存失败')\n    }\n  }\n\n\n  const handleResetToDefaults = async () => {\n    if (window.confirm('确定要重置所有设置到默认值吗？此操作不可撤销。')) {\n      const success = await resetToDefaults()\n      if (success) {\n        toast.success('所有设置已重置为默认值')\n      } else {\n        toast.error('重置失败')\n      }\n    }\n  }\n\n  if (isLoading) {\n    return (\n      <div className=\"p-6\">\n        <div className=\"animate-pulse\">\n          <div className=\"h-4 bg-gray-200 rounded w-1/4 mb-4\"></div>\n          <div className=\"space-y-3\">\n            <div className=\"h-16 bg-gray-200 rounded\"></div>\n            <div className=\"h-16 bg-gray-200 rounded\"></div>\n            <div className=\"h-16 bg-gray-200 rounded\"></div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"p-6\">\n      {/* 页面标题 */}\n      <div className=\"mb-8\">\n        <div className=\"flex items-center space-x-3 mb-2\">\n          <CogIcon className=\"w-6 h-6 text-blue-600\" />\n          <h1 className=\"text-2xl font-semibold text-gray-900\">基本设置</h1>\n        </div>\n        <p className=\"text-gray-500\">管理插件的基本配置选项</p>\n      </div>\n\n      <div className=\"space-y-8\">\n        {/* 显示设置 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-gray-900 mb-4\">显示设置</h2>\n          <div className=\"space-y-6\">\n            {/* 默认货币单位 */}\n            <div className=\"flex items-center justify-between py-4 border-b border-gray-100\">\n              <div className=\"flex items-center space-x-3\">\n                <GlobeAltIcon className=\"w-5 h-5 text-gray-400\" />\n                <div>\n                  <h3 className=\"text-sm font-medium text-gray-900\">货币单位</h3>\n                  <p className=\"text-sm text-gray-500\">设置余额和消费显示的默认货币单位</p>\n                </div>\n              </div>\n              <div className=\"flex bg-gray-100 rounded-lg p-1\">\n                <button\n                  onClick={() => handleCurrencyChange('USD')}\n                  className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n                    currencyType === 'USD'\n                      ? 'bg-white text-gray-900 shadow-sm'\n                      : 'text-gray-500 hover:text-gray-700'\n                  }`}\n                >\n                  美元 ($)\n                </button>\n                <button\n                  onClick={() => handleCurrencyChange('CNY')}\n                  className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n                    currencyType === 'CNY'\n                      ? 'bg-white text-gray-900 shadow-sm'\n                      : 'text-gray-500 hover:text-gray-700'\n                  }`}\n                >\n                  人民币 (¥)\n                </button>\n              </div>\n            </div>\n\n            {/* 默认标签页 */}\n            <div className=\"flex items-center justify-between py-4 border-b border-gray-100\">\n              <div className=\"flex items-center space-x-3\">\n                <EyeIcon className=\"w-5 h-5 text-gray-400\" />\n                <div>\n                  <h3 className=\"text-sm font-medium text-gray-900\">默认标签页</h3>\n                  <p className=\"text-sm text-gray-500\">设置插件启动时显示的默认标签页</p>\n                </div>\n              </div>\n              <div className=\"flex bg-gray-100 rounded-lg p-1\">\n                <button\n                  onClick={() => handleDefaultTabChange('consumption')}\n                  className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n                    activeTab === 'consumption'\n                      ? 'bg-white text-gray-900 shadow-sm'\n                      : 'text-gray-500 hover:text-gray-700'\n                  }`}\n                >\n                  今日消耗\n                </button>\n                <button\n                  onClick={() => handleDefaultTabChange('balance')}\n                  className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n                    activeTab === 'balance'\n                      ? 'bg-white text-gray-900 shadow-sm'\n                      : 'text-gray-500 hover:text-gray-700'\n                  }`}\n                >\n                  总余额\n                </button>\n              </div>\n            </div>\n\n          </div>\n        </section>\n\n        {/* 刷新设置 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-gray-900 mb-4\">刷新设置</h2>\n          <div className=\"space-y-6\">\n            {/* 自动刷新 */}\n            <div className=\"flex items-center justify-between py-4 border-b border-gray-100\">\n              <div>\n                <h3 className=\"text-sm font-medium text-gray-900\">自动刷新</h3>\n                <p className=\"text-sm text-gray-500\">定期自动刷新账号数据</p>\n              </div>\n              <Switch\n                checked={autoRefresh}\n                onChange={handleAutoRefreshChange}\n                className={`${\n                  autoRefresh ? 'bg-blue-600' : 'bg-gray-200'\n                } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}\n              >\n                <span\n                  className={`${\n                    autoRefresh ? 'translate-x-6' : 'translate-x-1'\n                  } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}\n                />\n              </Switch>\n            </div>\n\n            {/* 刷新间隔 */}\n            {autoRefresh && (\n              <div className=\"flex items-center justify-between py-4 border-b border-gray-100\">\n                <div>\n                  <h3 className=\"text-sm font-medium text-gray-900\">刷新间隔</h3>\n                  <p className=\"text-sm text-gray-500\">设置自动刷新的时间间隔（默认360秒，建议不要设置过小以避免频繁请求）</p>\n                </div>\n                <div className=\"flex items-center space-x-2\">\n                  <input\n                    type=\"number\"\n                    min=\"10\"\n                    value={intervalInput}\n                    onChange={(e) => handleRefreshIntervalChange(e.target.value)}\n                    onBlur={handleRefreshIntervalBlur}\n                    onKeyDown={(e) => {\n                      if (e.key === 'Enter') {\n                        e.currentTarget.blur() // 触发onBlur事件\n                      }\n                    }}\n                    placeholder=\"360\"\n                    className=\"w-20 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n                  />\n                  <span className=\"text-sm text-gray-500\">秒</span>\n                </div>\n              </div>\n            )}\n\n            {/* 打开插件时自动刷新 */}\n            <div className=\"flex items-center justify-between py-4 border-b border-gray-100\">\n              <div>\n                <h3 className=\"text-sm font-medium text-gray-900\">打开插件时自动刷新</h3>\n                <p className=\"text-sm text-gray-500\">当打开插件弹出层时自动刷新账号数据</p>\n              </div>\n              <Switch\n                checked={refreshOnOpen}\n                onChange={handleRefreshOnOpenChange}\n                className={`${\n                  refreshOnOpen ? 'bg-blue-600' : 'bg-gray-200'\n                } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}\n              >\n                <span\n                  className={`${\n                    refreshOnOpen ? 'translate-x-6' : 'translate-x-1'\n                  } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}\n                />\n              </Switch>\n            </div>\n          </div>\n        </section>\n\n        {/* 危险操作 */}\n        <section>\n          <h2 className=\"text-lg font-medium text-red-600 mb-4\">危险操作</h2>\n          <div className=\"bg-red-50 border border-red-200 rounded-lg p-4\">\n            <div className=\"flex items-center justify-between\">\n              <div>\n                <h3 className=\"text-sm font-medium text-red-800\">重置所有设置</h3>\n                <p className=\"text-sm text-red-600 mt-1\">\n                  将所有配置重置为默认值，此操作不可撤销\n                </p>\n              </div>\n              <button\n                onClick={handleResetToDefaults}\n                className=\"px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors\"\n              >\n                重置设置\n              </button>\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "options/pages/ImportExport.tsx",
    "content": "import { useState } from \"react\"\nimport { \n  ArrowPathIcon,\n  ArrowUpTrayIcon,\n  ArrowDownTrayIcon,\n  DocumentIcon,\n  ExclamationTriangleIcon,\n  CheckCircleIcon\n} from \"@heroicons/react/24/outline\"\nimport { accountStorage } from \"../../services/accountStorage\"\nimport { userPreferences } from \"../../services/userPreferences\"\nimport toast from 'react-hot-toast'\n\nexport default function ImportExport() {\n  const [isExporting, setIsExporting] = useState(false)\n  const [isImporting, setIsImporting] = useState(false)\n  const [importData, setImportData] = useState(\"\")\n\n  // 导出所有数据\n  const handleExportAll = async () => {\n    try {\n      setIsExporting(true)\n      \n      // 获取账号数据和用户偏好设置\n      const [accountData, preferencesData] = await Promise.all([\n        accountStorage.exportData(),\n        userPreferences.exportPreferences()\n      ])\n\n      const exportData = {\n        version: \"1.0\",\n        timestamp: Date.now(),\n        accounts: accountData,\n        preferences: preferencesData\n      }\n\n      // 创建下载链接\n      const blob = new Blob([JSON.stringify(exportData, null, 2)], {\n        type: 'application/json'\n      })\n      const url = URL.createObjectURL(blob)\n      const link = document.createElement('a')\n      link.href = url\n      link.download = `one-api-Hub-backup-${new Date().toISOString().split('T')[0]}.json`\n      document.body.appendChild(link)\n      link.click()\n      document.body.removeChild(link)\n      URL.revokeObjectURL(url)\n\n      toast.success('数据导出成功')\n    } catch (error) {\n      console.error('导出失败:', error)\n      toast.error('导出失败，请重试')\n    } finally {\n      setIsExporting(false)\n    }\n  }\n\n  // 导出账号数据\n  const handleExportAccounts = async () => {\n    try {\n      setIsExporting(true)\n      \n      const accountData = await accountStorage.exportData()\n      const exportData = {\n        version: \"1.0\",\n        timestamp: Date.now(),\n        type: \"accounts\",\n        data: accountData\n      }\n\n      const blob = new Blob([JSON.stringify(exportData, null, 2)], {\n        type: 'application/json'\n      })\n      const url = URL.createObjectURL(blob)\n      const link = document.createElement('a')\n      link.href = url\n      link.download = `accounts-backup-${new Date().toISOString().split('T')[0]}.json`\n      document.body.appendChild(link)\n      link.click()\n      document.body.removeChild(link)\n      URL.revokeObjectURL(url)\n\n      toast.success('账号数据导出成功')\n    } catch (error) {\n      console.error('导出账号数据失败:', error)\n      toast.error('导出失败，请重试')\n    } finally {\n      setIsExporting(false)\n    }\n  }\n\n  // 导出用户设置\n  const handleExportPreferences = async () => {\n    try {\n      setIsExporting(true)\n      \n      const preferencesData = await userPreferences.exportPreferences()\n      const exportData = {\n        version: \"1.0\",\n        timestamp: Date.now(),\n        type: \"preferences\",\n        data: preferencesData\n      }\n\n      const blob = new Blob([JSON.stringify(exportData, null, 2)], {\n        type: 'application/json'\n      })\n      const url = URL.createObjectURL(blob)\n      const link = document.createElement('a')\n      link.href = url\n      link.download = `preferences-backup-${new Date().toISOString().split('T')[0]}.json`\n      document.body.appendChild(link)\n      link.click()\n      document.body.removeChild(link)\n      URL.revokeObjectURL(url)\n\n      toast.success('用户设置导出成功')\n    } catch (error) {\n      console.error('导出用户设置失败:', error)\n      toast.error('导出失败，请重试')\n    } finally {\n      setIsExporting(false)\n    }\n  }\n\n  // 处理文件导入\n  const handleFileImport = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0]\n    if (!file) return\n\n    const reader = new FileReader()\n    reader.onload = (e) => {\n      const content = e.target?.result as string\n      setImportData(content)\n    }\n    reader.readAsText(file)\n  }\n\n  // 导入数据\n  const handleImport = async () => {\n    if (!importData.trim()) {\n      toast.error('请选择要导入的文件或输入数据')\n      return\n    }\n\n    try {\n      setIsImporting(true)\n      \n      const data = JSON.parse(importData)\n      \n      // 验证数据格式\n      if (!data.version || !data.timestamp) {\n        throw new Error('数据格式不正确')\n      }\n\n      let importSuccess = false\n\n      // 根据数据类型进行导入\n      if (data.accounts || !data.type) {\n        // 导入账号数据\n        const accountsData = data.accounts || data.data\n        if (accountsData) {\n          const success = await accountStorage.importData(accountsData)\n          if (success) {\n            importSuccess = true\n            toast.success('账号数据导入成功')\n          }\n        }\n      }\n\n      if (data.preferences || data.type === \"preferences\") {\n        // 导入用户设置\n        const preferencesData = data.preferences || data.data\n        if (preferencesData) {\n          const success = await userPreferences.importPreferences(preferencesData)\n          if (success) {\n            importSuccess = true\n            toast.success('用户设置导入成功')\n          }\n        }\n      }\n\n      if (!importSuccess) {\n        throw new Error('没有找到可导入的数据')\n      }\n\n      // 清空输入框\n      setImportData(\"\")\n      \n    } catch (error) {\n      console.error('导入失败:', error)\n      if (error instanceof SyntaxError) {\n        toast.error('数据格式错误，请检查JSON格式')\n      } else {\n        toast.error(`导入失败: ${error.message}`)\n      }\n    } finally {\n      setIsImporting(false)\n    }\n  }\n\n  // 验证导入数据\n  const validateImportData = () => {\n    if (!importData.trim()) return null\n    \n    try {\n      const data = JSON.parse(importData)\n      return {\n        valid: true,\n        hasAccounts: !!(data.accounts || (data.type !== \"preferences\" && data.data)),\n        hasPreferences: !!(data.preferences || data.type === \"preferences\"),\n        timestamp: data.timestamp ? new Date(data.timestamp).toLocaleString('zh-CN') : '未知'\n      }\n    } catch {\n      return { valid: false }\n    }\n  }\n\n  const validation = validateImportData()\n\n  return (\n    <div className=\"p-6\">\n      {/* 页面标题 */}\n      <div className=\"mb-8\">\n        <div className=\"flex items-center space-x-3 mb-2\">\n          <ArrowPathIcon className=\"w-6 h-6 text-blue-600\" />\n          <h1 className=\"text-2xl font-semibold text-gray-900\">导入/导出</h1>\n        </div>\n        <p className=\"text-gray-500\">备份和恢复插件数据</p>\n      </div>\n\n      <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-8\">\n        {/* 导出数据 */}\n        <section>\n          <div className=\"bg-white border border-gray-200 rounded-lg overflow-hidden\">\n            <div className=\"px-6 py-4 border-b border-gray-200\">\n              <div className=\"flex items-center space-x-2\">\n                <ArrowUpTrayIcon className=\"w-5 h-5 text-green-600\" />\n                <h2 className=\"text-lg font-medium text-gray-900\">导出数据</h2>\n              </div>\n              <p className=\"text-sm text-gray-500 mt-1\">将数据导出为JSON文件进行备份</p>\n            </div>\n            \n            <div className=\"p-6 space-y-4\">\n              {/* 导出所有数据 */}\n              <div className=\"border border-gray-200 rounded-lg p-4\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h3 className=\"font-medium text-gray-900 mb-1\">完整备份</h3>\n                    <p className=\"text-sm text-gray-500\">\n                      导出所有账号数据和用户设置，推荐用于完整备份\n                    </p>\n                  </div>\n                  <button\n                    onClick={handleExportAll}\n                    disabled={isExporting}\n                    className=\"ml-4 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors disabled:opacity-50\"\n                  >\n                    {isExporting ? '导出中...' : '导出'}\n                  </button>\n                </div>\n              </div>\n\n              {/* 导出账号数据 */}\n              <div className=\"border border-gray-200 rounded-lg p-4\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h3 className=\"font-medium text-gray-900 mb-1\">账号数据</h3>\n                    <p className=\"text-sm text-gray-500\">\n                      仅导出账号信息和相关数据\n                    </p>\n                  </div>\n                  <button\n                    onClick={handleExportAccounts}\n                    disabled={isExporting}\n                    className=\"ml-4 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50\"\n                  >\n                    {isExporting ? '导出中...' : '导出'}\n                  </button>\n                </div>\n              </div>\n\n              {/* 导出用户设置 */}\n              <div className=\"border border-gray-200 rounded-lg p-4\">\n                <div className=\"flex items-start justify-between\">\n                  <div className=\"flex-1\">\n                    <h3 className=\"font-medium text-gray-900 mb-1\">用户设置</h3>\n                    <p className=\"text-sm text-gray-500\">\n                      仅导出界面设置和偏好配置\n                    </p>\n                  </div>\n                  <button\n                    onClick={handleExportPreferences}\n                    disabled={isExporting}\n                    className=\"ml-4 px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors disabled:opacity-50\"\n                  >\n                    {isExporting ? '导出中...' : '导出'}\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        {/* 导入数据 */}\n        <section>\n          <div className=\"bg-white border border-gray-200 rounded-lg overflow-hidden\">\n            <div className=\"px-6 py-4 border-b border-gray-200\">\n              <div className=\"flex items-center space-x-2\">\n                <ArrowDownTrayIcon className=\"w-5 h-5 text-blue-600\" />\n                <h2 className=\"text-lg font-medium text-gray-900\">导入数据</h2>\n              </div>\n              <p className=\"text-sm text-gray-500 mt-1\">从备份文件恢复数据</p>\n            </div>\n            \n            <div className=\"p-6 space-y-4\">\n              {/* 文件选择 */}\n              <div>\n                <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                  选择备份文件\n                </label>\n                <div className=\"flex items-center space-x-3\">\n                  <input\n                    type=\"file\"\n                    accept=\".json\"\n                    onChange={handleFileImport}\n                    className=\"block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100\"\n                  />\n                  <DocumentIcon className=\"w-5 h-5 text-gray-400\" />\n                </div>\n              </div>\n\n              {/* 数据预览 */}\n              <div>\n                <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                  数据内容预览\n                </label>\n                <textarea\n                  value={importData}\n                  onChange={(e) => setImportData(e.target.value)}\n                  placeholder=\"粘贴JSON数据或通过上面的文件选择器导入...\"\n                  className=\"w-full h-32 px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n                />\n              </div>\n\n              {/* 数据验证结果 */}\n              {validation && (\n                <div className={`p-3 rounded-lg ${\n                  validation.valid \n                    ? 'bg-green-50 border border-green-200' \n                    : 'bg-red-50 border border-red-200'\n                }`}>\n                  <div className=\"flex items-start space-x-2\">\n                    {validation.valid ? (\n                      <CheckCircleIcon className=\"w-5 h-5 text-green-600 mt-0.5 flex-shrink-0\" />\n                    ) : (\n                      <ExclamationTriangleIcon className=\"w-5 h-5 text-red-600 mt-0.5 flex-shrink-0\" />\n                    )}\n                    <div className=\"text-sm\">\n                      {validation.valid ? (\n                        <div>\n                          <p className=\"text-green-800 font-medium\">数据格式正确</p>\n                          <div className=\"mt-1 text-green-700\">\n                            {validation.hasAccounts && <p>• 包含账号数据</p>}\n                            {validation.hasPreferences && <p>• 包含用户设置</p>}\n                            <p>• 备份时间: {validation.timestamp}</p>\n                          </div>\n                        </div>\n                      ) : (\n                        <p className=\"text-red-800\">数据格式错误，请检查JSON格式</p>\n                      )}\n                    </div>\n                  </div>\n                </div>\n              )}\n\n              {/* 导入按钮 */}\n              <button\n                onClick={handleImport}\n                disabled={isImporting || !validation?.valid}\n                className=\"w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n              >\n                {isImporting ? '导入中...' : '导入数据'}\n              </button>\n            </div>\n          </div>\n        </section>\n      </div>\n\n      {/* 重要提示 */}\n      <div className=\"mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg\">\n        <div className=\"flex items-start space-x-3\">\n          <ExclamationTriangleIcon className=\"w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0\" />\n          <div className=\"text-sm\">\n            <p className=\"text-yellow-800 font-medium mb-1\">重要提示</p>\n            <ul className=\"text-yellow-700 space-y-1\">\n              <li>• 导入数据将覆盖现有的相同类型数据，请谨慎操作</li>\n              <li>• 建议在导入前先导出当前数据进行备份</li>\n              <li>• 仅支持本插件导出的JSON格式文件</li>\n              <li>• 导入的账号数据包含敏感信息，请确保文件来源可信</li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}"
  },
  {
    "path": "options/pages/KeyManagement.tsx",
    "content": "import { useState, useEffect } from \"react\"\nimport { \n  KeyIcon, \n  MagnifyingGlassIcon, \n  PlusIcon,\n  DocumentDuplicateIcon,\n  PencilIcon,\n  TrashIcon,\n  EyeIcon,\n  EyeSlashIcon\n} from \"@heroicons/react/24/outline\"\nimport { useAccountData } from \"../../hooks/useAccountData\"\nimport { fetchAccountTokens, deleteApiToken, type ApiToken } from \"../../services/apiService\"\nimport type { DisplaySiteData } from \"../../types\"\nimport AddTokenDialog from \"../../components/AddTokenDialog\"\nimport toast from 'react-hot-toast'\n\nexport default function KeyManagement({ routeParams }: { routeParams?: Record<string, string> }) {\n  const { displayData } = useAccountData()\n  const [selectedAccount, setSelectedAccount] = useState<string>(\"\") // 改为空字符串，不默认选择\n  const [searchTerm, setSearchTerm] = useState(\"\")\n  const [tokens, setTokens] = useState<(ApiToken & { accountName: string })[]>([])\n  const [isLoading, setIsLoading] = useState(false)\n  const [visibleKeys, setVisibleKeys] = useState<Set<number>>(new Set())\n  const [isAddTokenOpen, setIsAddTokenOpen] = useState(false)\n  const [editingToken, setEditingToken] = useState<(ApiToken & { accountName: string }) | null>(null)\n\n  // 加载选中账号的密钥\n  const loadTokens = async (accountId?: string) => {\n    const targetAccountId = accountId || selectedAccount\n    if (!targetAccountId || displayData.length === 0) return\n    \n    setIsLoading(true)\n    try {\n      // 只加载选中账号的密钥\n      const account = displayData.find(acc => acc.id === targetAccountId)\n      if (!account) {\n        setTokens([])\n        return\n      }\n\n      const accountTokens = await fetchAccountTokens(\n        account.baseUrl,\n        account.userId,\n        account.token\n      )\n      \n      const tokensWithAccount = accountTokens.map(token => ({\n        ...token,\n        accountName: account.name\n      }))\n      \n      setTokens(tokensWithAccount)\n    } catch (error) {\n      console.error(`获取账号密钥失败:`, error)\n      toast.error('加载密钥列表失败')\n      setTokens([])\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  // 账号选择变化时加载密钥\n  useEffect(() => {\n    if (selectedAccount) {\n      loadTokens()\n    } else {\n      setTokens([]) // 清空密钥列表\n    }\n  }, [selectedAccount, displayData])\n\n  // 处理路由参数中的账号ID\n  useEffect(() => {\n    if (routeParams?.accountId && displayData.length > 0) {\n      // 验证账号ID是否存在\n      const accountExists = displayData.some(acc => acc.id === routeParams.accountId)\n      if (accountExists) {\n        setSelectedAccount(routeParams.accountId)\n      }\n    }\n  }, [routeParams?.accountId, displayData])\n\n  // 过滤密钥 (现在只需要搜索过滤，因为已经只加载选中账号的密钥)\n  const filteredTokens = tokens.filter(token => {\n    return token.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n           token.key.toLowerCase().includes(searchTerm.toLowerCase())\n  })\n\n  // 复制密钥\n  const copyKey = async (key: string, name: string) => {\n    try {\n      const textToCopy = key.startsWith('sk-') ? key : 'sk-' + key;\n      await navigator.clipboard.writeText(textToCopy)\n      toast.success(`密钥 ${name} 已复制到剪贴板`)\n    } catch (error) {\n      toast.error('复制失败')\n    }\n  }\n\n  // 切换密钥可见性\n  const toggleKeyVisibility = (tokenId: number) => {\n    setVisibleKeys(prev => {\n      const newSet = new Set(prev)\n      if (newSet.has(tokenId)) {\n        newSet.delete(tokenId)\n      } else {\n        newSet.add(tokenId)\n      }\n      return newSet\n    })\n  }\n\n  // 处理添加密钥\n  const handleAddToken = () => {\n    setIsAddTokenOpen(true)\n  }\n\n  // 关闭添加密钥对话框\n  const handleCloseAddToken = () => {\n    setIsAddTokenOpen(false)\n    setEditingToken(null) // 清除编辑状态\n    // 重新加载当前选中账号的密钥列表\n    if (selectedAccount) {\n      loadTokens()\n    }\n  }\n\n  // 处理编辑密钥\n  const handleEditToken = (token: ApiToken & { accountName: string }) => {\n    setEditingToken(token)\n    setIsAddTokenOpen(true)\n  }\n\n  // 处理删除密钥\n  const handleDeleteToken = async (token: ApiToken & { accountName: string }) => {\n    if (!window.confirm(`确定要删除密钥 \"${token.name}\" 吗？此操作不可撤销。`)) {\n      return\n    }\n\n    try {\n      // 找到对应的账号信息\n      const account = displayData.find(acc => acc.name === token.accountName)\n      if (!account) {\n        toast.error('找不到对应账号信息')\n        return\n      }\n\n      await deleteApiToken(account.baseUrl, account.userId, account.token, token.id)\n      toast.success(`密钥 \"${token.name}\" 删除成功`)\n      \n      // 重新加载当前选中账号的密钥列表\n      if (selectedAccount) {\n        loadTokens()\n      }\n    } catch (error) {\n      console.error('删除密钥失败:', error)\n      toast.error('删除密钥失败，请稍后重试')\n    }\n  }\n\n  // 格式化密钥显示\n  const formatKey = (key: string, tokenId: number) => {\n    if (visibleKeys.has(tokenId)) {\n      return key\n    }\n    return `${key.substring(0, 8)}${'*'.repeat(16)}${key.substring(key.length - 4)}`\n  }\n\n  // 格式化时间\n  const formatTime = (timestamp: number) => {\n    if (timestamp <= 0) return '永不过期'\n    return new Date(timestamp * 1000).toLocaleDateString('zh-CN')\n  }\n\n  // 格式化额度\n  const formatQuota = (quota: number, unlimited: boolean) => {\n    if (unlimited || quota < 0) return '无限额度'\n    return `$${(quota / 500000).toFixed(2)}`\n  }\n\n  return (\n    <div className=\"p-6\">\n      {/* 页面标题 */}\n      <div className=\"mb-8\">\n        <div className=\"flex items-center justify-between mb-2\">\n          <div className=\"flex items-center space-x-3\">\n            <KeyIcon className=\"w-6 h-6 text-blue-600\" />\n            <h1 className=\"text-2xl font-semibold text-gray-900\">密钥管理</h1>\n          </div>\n          <div className=\"flex items-center space-x-3\">\n            <button\n              onClick={handleAddToken}\n              disabled={!selectedAccount || displayData.length === 0}\n              className=\"px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2\"\n            >\n              <PlusIcon className=\"w-4 h-4\" />\n              <span>添加密钥</span>\n            </button>\n            <button\n              onClick={() => selectedAccount && loadTokens()}\n              disabled={isLoading || !selectedAccount}\n              className=\"px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50\"\n            >\n              {isLoading ? '刷新中...' : '刷新列表'}\n            </button>\n          </div>\n        </div>\n        <p className=\"text-gray-500\">选择账号后查看和管理该账号的API密钥</p>\n      </div>\n\n      {/* 账号选择和搜索 */}\n      <div className=\"mb-6 space-y-4\">\n        {/* 账号选择 */}\n        <div className=\"mb-4\">\n          <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n            选择账号\n          </label>\n          <select\n            value={selectedAccount}\n            onChange={(e) => setSelectedAccount(e.target.value)}\n            className=\"w-full sm:w-80 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n          >\n            <option value=\"\">请选择账号</option>\n            {displayData.map(account => (\n              <option key={account.id} value={account.id}>{account.name}</option>\n            ))}\n          </select>\n        </div>\n\n        {/* 搜索框 */}\n        <div className=\"flex flex-col sm:flex-row gap-4\">\n          <div className=\"flex-1 relative\">\n            <MagnifyingGlassIcon className=\"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400\" />\n            <input\n              type=\"text\"\n              placeholder=\"搜索密钥名称...\"\n              value={searchTerm}\n              onChange={(e) => setSearchTerm(e.target.value)}\n              disabled={!selectedAccount}\n              className=\"w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed\"\n            />\n          </div>\n        </div>\n\n        {/* 统计信息 */}\n        {selectedAccount && (\n          <div className=\"flex items-center space-x-6 text-sm text-gray-500\">\n            <span>总计 {tokens.length} 个密钥</span>\n            <span>启用 {tokens.filter(t => t.status === 1).length} 个</span>\n            <span>显示 {filteredTokens.length} 个</span>\n          </div>\n        )}\n      </div>\n\n      {/* 密钥列表 */}\n      {!selectedAccount ? (\n        <div className=\"text-center py-12\">\n          <KeyIcon className=\"w-12 h-12 text-gray-300 mx-auto mb-4\" />\n          <p className=\"text-gray-500\">请先选择一个账号查看密钥列表</p>\n        </div>\n      ) : isLoading ? (\n        <div className=\"space-y-3\">\n          {[...Array(3)].map((_, i) => (\n            <div key={i} className=\"border border-gray-200 rounded-lg p-4 animate-pulse\">\n              <div className=\"h-4 bg-gray-200 rounded w-1/4 mb-2\"></div>\n              <div className=\"h-3 bg-gray-200 rounded w-1/2 mb-2\"></div>\n              <div className=\"h-3 bg-gray-200 rounded w-3/4\"></div>\n            </div>\n          ))}\n        </div>\n      ) : filteredTokens.length === 0 ? (\n        <div className=\"text-center py-12\">\n          <KeyIcon className=\"w-12 h-12 text-gray-300 mx-auto mb-4\" />\n          <p className=\"text-gray-500 mb-4\">\n            {tokens.length === 0 ? '暂无密钥数据' : '没有找到匹配的密钥'}\n          </p>\n          {displayData.length === 0 ? (\n            <p className=\"text-sm text-gray-400\">请先添加账号</p>\n          ) : tokens.length === 0 ? (\n            <button\n              onClick={handleAddToken}\n              className=\"px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors flex items-center space-x-2 mx-auto\"\n            >\n              <PlusIcon className=\"w-4 h-4\" />\n              <span>创建第一个密钥</span>\n            </button>\n          ) : null}\n        </div>\n      ) : (\n        <div className=\"space-y-3\">\n          {filteredTokens.map((token) => (\n            <div\n              key={`${token.accountName}-${token.id}`}\n              className=\"border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors\"\n            >\n              <div className=\"flex items-start justify-between\">\n                <div className=\"flex-1 min-w-0\">\n                  {/* 密钥名称和状态 */}\n                  <div className=\"flex items-center space-x-3 mb-2\">\n                    <h3 className=\"text-lg font-medium text-gray-900 truncate\">\n                      {token.name}\n                    </h3>\n                    <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${\n                      token.status === 1 \n                        ? 'bg-green-100 text-green-800' \n                        : 'bg-red-100 text-red-800'\n                    }`}>\n                      {token.status === 1 ? '启用' : '禁用'}\n                    </span>\n                    <span className=\"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800\">\n                      {token.accountName}\n                    </span>\n                  </div>\n\n                  {/* 密钥信息 */}\n                  <div className=\"space-y-2 text-sm text-gray-600\">\n                    <div className=\"flex items-center space-x-4\">\n                      <div className=\"flex items-center space-x-2\">\n                        <span className=\"text-gray-500\">密钥:</span>\n                        <code className=\"bg-gray-100 px-2 py-1 rounded font-mono text-xs\">\n                          {formatKey(token.key, token.id)}\n                        </code>\n                        <button\n                          onClick={() => toggleKeyVisibility(token.id)}\n                          className=\"p-1 text-gray-400 hover:text-gray-600\"\n                        >\n                          {visibleKeys.has(token.id) ? (\n                            <EyeSlashIcon className=\"w-4 h-4\" />\n                          ) : (\n                            <EyeIcon className=\"w-4 h-4\" />\n                          )}\n                        </button>\n                      </div>\n                    </div>\n\n                    <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n                      <div>\n                        <span className=\"text-gray-500\">剩余额度:</span>\n                        <span className=\"ml-2 font-medium\">\n                          {formatQuota(token.remain_quota, token.unlimited_quota)}\n                        </span>\n                      </div>\n                      <div>\n                        <span className=\"text-gray-500\">已用额度:</span>\n                        <span className=\"ml-2 font-medium\">\n                          {formatQuota(token.used_quota, false)}\n                        </span>\n                      </div>\n                      <div>\n                        <span className=\"text-gray-500\">过期时间:</span>\n                        <span className=\"ml-2 font-medium\">\n                          {formatTime(token.expired_time)}\n                        </span>\n                      </div>\n                      <div>\n                        <span className=\"text-gray-500\">创建时间:</span>\n                        <span className=\"ml-2 font-medium\">\n                          {formatTime(token.created_time)}\n                        </span>\n                      </div>\n                    </div>\n\n                    {token.group && (\n                      <div>\n                        <span className=\"text-gray-500\">分组:</span>\n                        <span className=\"ml-2 font-medium\">{token.group}</span>\n                      </div>\n                    )}\n                  </div>\n                </div>\n\n                {/* 操作按钮 */}\n                <div className=\"flex items-center space-x-2 ml-4\">\n                  <button\n                    onClick={() => copyKey(token.key, token.name)}\n                    className=\"p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors\"\n                    title=\"复制密钥\"\n                  >\n                    <DocumentDuplicateIcon className=\"w-4 h-4\" />\n                  </button>\n                  <button\n                    onClick={() => handleEditToken(token)}\n                    className=\"p-2 text-blue-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors\"\n                    title=\"编辑密钥\"\n                  >\n                    <PencilIcon className=\"w-4 h-4\" />\n                  </button>\n                  <button\n                    onClick={() => handleDeleteToken(token)}\n                    className=\"p-2 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors\"\n                    title=\"删除密钥\"\n                  >\n                    <TrashIcon className=\"w-4 h-4\" />\n                  </button>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      )}\n\n      {/* 说明文字 */}\n      <div className=\"mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg\">\n        <div className=\"flex items-start space-x-3\">\n          <KeyIcon className=\"w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0\" />\n          <div className=\"text-sm\">\n            <p className=\"text-yellow-800 font-medium mb-1\">密钥管理说明</p>\n            <p className=\"text-yellow-700\">\n              此页面显示所有账号的API密钥信息，包括使用情况和过期时间。\n              可以通过右上角的\"添加密钥\"按钮或点击各密钥项目旁的\"+\"按钮为指定账号创建新密钥。\n              请妥善保管您的API密钥，避免泄露给他人。\n            </p>\n          </div>\n        </div>\n      </div>\n\n      {/* 添加密钥对话框 */}\n      <AddTokenDialog\n        isOpen={isAddTokenOpen}\n        onClose={handleCloseAddToken}\n        availableAccounts={displayData.map(account => ({\n          id: account.id,\n          name: account.name,\n          baseUrl: account.baseUrl,\n          userId: account.userId,\n          token: account.token\n        }))}\n        preSelectedAccountId={selectedAccount || null}\n        editingToken={editingToken}\n      />\n    </div>\n  )\n}"
  },
  {
    "path": "options/pages/ModelList.tsx",
    "content": "import { useState, useEffect, useMemo, useRef } from \"react\"\nimport { \n  CpuChipIcon, \n  MagnifyingGlassIcon, \n  AdjustmentsHorizontalIcon,\n  EyeIcon,\n  EyeSlashIcon,\n  ArrowPathIcon,\n  ExclamationTriangleIcon\n} from \"@heroicons/react/24/outline\"\nimport { Tab } from '@headlessui/react'\nimport toast from 'react-hot-toast'\nimport { useAccountData } from \"../../hooks/useAccountData\"\nimport { \n  fetchModelPricing, \n  type ModelPricing, \n  type PricingResponse \n} from \"../../services/apiService\"\nimport {\n  getAllProviders,\n  filterModelsByProvider,\n  getProviderConfig,\n  type ProviderType \n} from \"../../utils/modelProviders\"\nimport { \n  calculateModelPrice,\n  type CalculatedPrice \n} from \"../../utils/modelPricing\"\nimport ModelItem from \"../../components/ModelItem\"\n\nexport default function ModelList({ routeParams }: { routeParams?: Record<string, string> }) {\n  const { displayData } = useAccountData()\n  \n  // 状态管理\n  const [selectedAccount, setSelectedAccount] = useState<string>(\"\")\n  const [searchTerm, setSearchTerm] = useState(\"\")\n  const [selectedProvider, setSelectedProvider] = useState<ProviderType | 'all'>('all')\n  const [selectedGroup, setSelectedGroup] = useState<string>('default')\n  const [isLoading, setIsLoading] = useState(false)\n  \n  // 数据状态\n  const [pricingData, setPricingData] = useState<PricingResponse | null>(null)\n  const [dataFormatError, setDataFormatError] = useState<boolean>(false)\n  \n  // 显示选项\n  const [showRealPrice, setShowRealPrice] = useState(false)\n  const [showRatioColumn, setShowRatioColumn] = useState(false)\n  const [showEndpointTypes, setShowEndpointTypes] = useState(false)\n  \n  // 安全获取账号数据\n  const safeDisplayData = displayData || []\n  \n  // 获取当前选中的账号信息\n  const currentAccount = safeDisplayData.find(acc => acc.id === selectedAccount)\n  \n  // 获取厂商列表\n  const providers = getAllProviders()\n  \n  // Tab滚动相关\n  const tabListRef = useRef<HTMLDivElement>(null)\n  \n  // 自动滚动到选中的Tab\n  const scrollToSelectedTab = (selectedIndex: number) => {\n    if (!tabListRef.current) return\n    \n    const tabList = tabListRef.current\n    const tabs = tabList.children\n    \n    if (selectedIndex >= 0 && selectedIndex < tabs.length) {\n      const selectedTab = tabs[selectedIndex] as HTMLElement\n      const tabListRect = tabList.getBoundingClientRect()\n      const selectedTabRect = selectedTab.getBoundingClientRect()\n      \n      // 计算当前tab相对于容器的位置\n      const tabLeft = selectedTabRect.left - tabListRect.left + tabList.scrollLeft\n      const tabRight = tabLeft + selectedTabRect.width\n      \n      // 计算理想的滚动位置（让选中的tab居中显示）\n      const containerWidth = tabList.clientWidth\n      const idealScrollLeft = tabLeft - (containerWidth / 2) + (selectedTabRect.width / 2)\n      \n      // 平滑滚动到目标位置\n      tabList.scrollTo({\n        left: Math.max(0, idealScrollLeft),\n        behavior: 'smooth'\n      })\n    }\n  }\n  \n  // 当选中的厂商改变时，自动滚动到对应位置\n  useEffect(() => {\n    const selectedIndex = selectedProvider === 'all' ? 0 : Math.max(0, providers.indexOf(selectedProvider as ProviderType) + 1)\n    setTimeout(() => scrollToSelectedTab(selectedIndex), 100)\n  }, [selectedProvider, providers])\n  \n  // 加载模型定价数据\n  const loadPricingData = async (accountId: string) => {\n    const account = safeDisplayData.find(acc => acc.id === accountId)\n    if (!account) return\n    \n    setIsLoading(true)\n    setDataFormatError(false)\n    try {\n      const data = await fetchModelPricing(account.baseUrl, account.userId, account.token)\n      console.log('API 响应数据:', data)\n      console.log('模型数据:', data.data)\n      console.log('分组比率:', data.group_ratio)\n      console.log('可用分组:', data.usable_group)\n      \n      // 检查数据格式是否正确\n      if (!Array.isArray(data.data)) {\n        console.error('模型数据格式错误，data 字段不是数组:', data.data)\n        setDataFormatError(true)\n        setPricingData(null)\n        toast.error('当前站点的模型数据格式不符合标准，请手动查看站点定价页面')\n        return\n      }\n      \n      setPricingData(data)\n      toast.success('模型数据加载成功')\n    } catch (error) {\n      console.error('加载模型数据失败:', error)\n      toast.error('加载模型数据失败，请稍后重试')\n      setPricingData(null)\n      setDataFormatError(false)\n    } finally {\n      setIsLoading(false)\n    }\n  }\n  \n  // 账号变化时重新加载数据\n  useEffect(() => {\n    if (selectedAccount) {\n      loadPricingData(selectedAccount)\n    } else {\n      setPricingData(null)\n    }\n  }, [selectedAccount, safeDisplayData])\n  \n  // 处理路由参数中的账号ID\n  useEffect(() => {\n    if (routeParams?.accountId && safeDisplayData.length > 0) {\n      // 验证账号ID是否存在\n      const accountExists = safeDisplayData.some(acc => acc.id === routeParams.accountId)\n      if (accountExists) {\n        setSelectedAccount(routeParams.accountId)\n      }\n    }\n  }, [routeParams?.accountId, safeDisplayData])\n  \n  // 当定价数据加载完成时，自动选择合适的分组\n  useEffect(() => {\n    if (pricingData && pricingData.group_ratio) {\n      const availableGroupsList = Object.keys(pricingData.group_ratio).filter(key => key !== '')\n      console.log('分组选择逻辑 - 当前选中分组:', selectedGroup)\n      console.log('分组选择逻辑 - 可用分组列表:', availableGroupsList)\n      \n      // 检查当前选中的分组是否在可用列表中\n      if (selectedGroup !== 'all' && !availableGroupsList.includes(selectedGroup)) {\n        console.log('分组选择逻辑 - 当前分组不存在，需要重新选择')\n        \n        if (availableGroupsList.includes('default')) {\n          // 如果有default分组，选择default\n          console.log('分组选择逻辑 - 选择default分组')\n          setSelectedGroup('default')\n        } else if (availableGroupsList.length > 0) {\n          // 如果没有default但有其他分组，选择第一个\n          console.log('分组选择逻辑 - 选择第一个可用分组:', availableGroupsList[0])\n          setSelectedGroup(availableGroupsList[0])\n        } else {\n          // 如果没有任何分组，选择\"所有分组\"\n          console.log('分组选择逻辑 - 没有可用分组，选择所有分组')\n          setSelectedGroup('all')\n        }\n      }\n    }\n  }, [pricingData, selectedGroup])\n  \n  // 计算模型价格\n  const modelsWithPricing = useMemo(() => {\n    console.log('计算模型价格 - pricingData:', pricingData)\n    console.log('计算模型价格 - currentAccount:', currentAccount)\n    \n    if (!pricingData || !currentAccount) {\n      console.log('缺少必要数据，返回空数组')\n      return []\n    }\n    \n    // 额外的数据安全检查\n    if (!Array.isArray(pricingData.data)) {\n      console.error('模型数据不是数组格式，无法处理:', pricingData.data)\n      return []\n    }\n    \n    console.log('开始处理模型数据，模型数量:', pricingData.data.length)\n    \n    return pricingData.data.map(model => {\n      // 安全的汇率计算\n      const exchangeRate = currentAccount?.balance?.USD > 0 \n        ? currentAccount.balance.CNY / currentAccount.balance.USD \n        : 7 // 默认汇率\n        \n      const calculatedPrice = calculateModelPrice(\n        model,\n        pricingData.group_ratio || {},\n        exchangeRate,\n        selectedGroup === 'all' ? 'default' : selectedGroup // 根据选中分组计算价格\n      )\n      \n      return {\n        model,\n        calculatedPrice\n      }\n    })\n  }, [pricingData, currentAccount, selectedGroup])\n  \n  // 基础过滤模型（不包含厂商过滤，用于Tab数量计算）\n  const baseFilteredModels = useMemo(() => {\n    console.log('基础过滤模型 - modelsWithPricing:', modelsWithPricing)\n    let filtered = modelsWithPricing\n    \n    // 按分组过滤\n    if (selectedGroup !== 'all') {\n      console.log('基础过滤-按分组过滤:', selectedGroup)\n      \n      // 额外的安全检查：确保选中的分组确实存在\n      const availableGroupsList = pricingData?.group_ratio ? Object.keys(pricingData.group_ratio).filter(key => key !== '') : []\n      if (!availableGroupsList.includes(selectedGroup)) {\n        console.warn('基础过滤-警告：选中的分组不存在于可用分组列表中', {\n          selectedGroup,\n          availableGroups: availableGroupsList\n        })\n        // 如果选中的分组不存在，不进行分组过滤，显示所有模型\n      } else {\n        filtered = filtered.filter(item => \n          item.model.enable_groups.includes(selectedGroup)\n        )\n      }\n      console.log('基础过滤-分组过滤后:', filtered.length)\n    }\n    \n    // 搜索过滤\n    if (searchTerm) {\n      console.log('基础过滤-搜索过滤:', searchTerm)\n      const searchLower = searchTerm.toLowerCase()\n      filtered = filtered.filter(item => \n        item.model.model_name.toLowerCase().includes(searchLower) ||\n        (item.model.model_description?.toLowerCase().includes(searchLower) || false)\n      )\n      console.log('基础过滤-搜索过滤后:', filtered.length)\n    }\n    \n    console.log('基础过滤结果:', filtered)\n    return filtered\n  }, [modelsWithPricing, selectedGroup, searchTerm, pricingData])\n\n  // 过滤和搜索模型（包含厂商过滤，用于实际显示）\n  const filteredModels = useMemo(() => {\n    console.log('过滤模型 - baseFilteredModels:', baseFilteredModels)\n    let filtered = baseFilteredModels\n    \n    // 按厂商过滤\n    if (selectedProvider !== 'all') {\n      console.log('按厂商过滤:', selectedProvider)\n      filtered = filtered.filter(item => \n        filterModelsByProvider([item.model], selectedProvider).length > 0\n      )\n      console.log('厂商过滤后:', filtered.length)\n    }\n    \n    console.log('最终过滤结果:', filtered)\n    return filtered\n  }, [baseFilteredModels, selectedProvider])\n  \n  // 处理模型item中的分组点击\n  const handleGroupClick = (group: string) => {\n    setSelectedGroup(group)\n  }\n  \n  // 计算指定厂商在当前过滤条件下的模型数量\n  const getProviderFilteredCount = (provider: ProviderType) => {\n    return baseFilteredModels.filter(item => \n      filterModelsByProvider([item.model], provider).length > 0\n    ).length\n  }\n  \n  // 获取可用分组列表\n  const availableGroups = useMemo(() => {\n    console.log('处理可用分组 - pricingData:', pricingData)\n    if (!pricingData || !pricingData.group_ratio) {\n      console.log('没有分组数据，返回空数组')\n      return []\n    }\n    // 从group_ratio中获取分组，并过滤掉空键\n    const groups = Object.keys(pricingData.group_ratio).filter(key => \n      key !== ''\n    )\n    console.log('原始分组数据:', pricingData.group_ratio)  \n    console.log('处理后的分组列表:', groups)\n    return groups\n  }, [pricingData])\n\n  return (\n    <div className=\"p-6\">\n      {/* 页面标题 */}\n      <div className=\"mb-6\">\n        <div className=\"flex items-center space-x-3 mb-2\">\n          <CpuChipIcon className=\"w-6 h-6 text-blue-600\" />\n          <h1 className=\"text-2xl font-semibold text-gray-900\">模型列表</h1>\n        </div>\n        <p className=\"text-gray-500\">查看和管理可用的AI模型</p>\n      </div>\n\n      {/* 账号选择 */}\n      <div className=\"mb-6\">\n        <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n          选择账号\n        </label>\n        <select\n          value={selectedAccount}\n          onChange={(e) => setSelectedAccount(e.target.value)}\n          className=\"w-full sm:w-80 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n        >\n          <option value=\"\">请选择账号</option>\n          {safeDisplayData.map(account => (\n            <option key={account.id} value={account.id}>{account.name}</option>\n          ))}\n        </select>\n      </div>\n\n      {/* 如果没有选择账号，显示提示 */}\n      {!selectedAccount && (\n        <div className=\"text-center py-12\">\n          <CpuChipIcon className=\"w-12 h-12 text-gray-300 mx-auto mb-4\" />\n          <p className=\"text-gray-500\">请先选择一个账号查看模型列表</p>\n        </div>\n      )}\n\n      {/* 加载状态 */}\n      {selectedAccount && isLoading && (\n        <div className=\"text-center py-12\">\n          <ArrowPathIcon className=\"w-8 h-8 text-blue-600 mx-auto mb-4 animate-spin\" />\n          <p className=\"text-gray-500\">正在加载模型数据...</p>\n        </div>\n      )}\n\n      {/* 数据格式错误提示 */}\n      {selectedAccount && !isLoading && dataFormatError && currentAccount && (\n        <div className=\"mb-6 p-6 bg-yellow-50 border border-yellow-200 rounded-lg\">\n          <div className=\"flex items-start space-x-4\">\n            <ExclamationTriangleIcon className=\"w-6 h-6 text-yellow-600 mt-1 flex-shrink-0\" />\n            <div className=\"flex-1\">\n              <h3 className=\"text-lg font-medium text-yellow-800 mb-2\">数据格式不兼容</h3>\n              <p className=\"text-yellow-700 mb-4\">\n                当前站点的模型数据接口返回格式不符合标准规范，可能是经过二次开发的站点。\n                插件暂时无法解析该站点的模型定价信息。\n              </p>\n              <div className=\"flex flex-col sm:flex-row gap-3\">\n                <a\n                  href={`${currentAccount.baseUrl}/pricing`}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"inline-flex items-center px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors\"\n                >\n                  <span>前往站点查看定价信息</span>\n                  <svg className=\"w-4 h-4 ml-2\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-2M17 3l4 4m-5 0l5-5\" />\n                  </svg>\n                </a>\n                <button\n                  onClick={() => loadPricingData(selectedAccount)}\n                  className=\"inline-flex items-center px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors\"\n                >\n                  <ArrowPathIcon className=\"w-4 h-4 mr-2\" />\n                  <span>重新尝试加载</span>\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* 模型数据展示 */}\n      {selectedAccount && !isLoading && pricingData && (\n        <>\n          {/* 控制面板 */}\n          <div className=\"mb-6 bg-white border border-gray-200 rounded-lg p-6 shadow-sm\">\n            {/* 第一行：主要过滤控件 */}\n            <div className=\"flex flex-col lg:flex-row gap-4 mb-6\">\n              {/* 搜索框 */}\n              <div className=\"flex-1\">\n                <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                  搜索模型\n                </label>\n                <div className=\"relative\">\n                  <MagnifyingGlassIcon className=\"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400\" />\n                  <input\n                    type=\"text\"\n                    placeholder=\"输入模型名称或描述...\"\n                    value={searchTerm}\n                    onChange={(e) => setSearchTerm(e.target.value)}\n                    className=\"w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n                  />\n                </div>\n              </div>\n\n              {/* 分组筛选 */}\n              <div className=\"w-full lg:w-64\">\n                <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n                  用户分组\n                </label>\n                <select\n                  value={selectedGroup}\n                  onChange={(e) => setSelectedGroup(e.target.value)}\n                  className=\"w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n                >\n                  <option value=\"all\">所有分组</option>\n                  {availableGroups.map(group => (\n                    <option key={group} value={group}>\n                      {group} ({pricingData?.group_ratio?.[group] || 1}x)\n                    </option>\n                  ))}\n                </select>\n              </div>\n\n              {/* 刷新按钮 */}\n              <div className=\"w-full lg:w-auto\">\n                <label className=\"block text-sm font-medium text-gray-700 mb-2 lg:invisible\">\n                  操作\n                </label>\n                <button\n                  onClick={() => loadPricingData(selectedAccount)}\n                  disabled={isLoading}\n                  className=\"w-full lg:w-auto px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center justify-center space-x-2\"\n                >\n                  <ArrowPathIcon className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />\n                  <span>刷新数据</span>\n                </button>\n              </div>\n            </div>\n\n            {/* 第二行：显示选项和统计信息 */}\n            <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 pt-4 border-t border-gray-100\">\n              {/* 显示选项 */}\n              <div className=\"flex flex-wrap items-center gap-4 text-sm\">\n                <div className=\"flex items-center space-x-2\">\n                  <AdjustmentsHorizontalIcon className=\"w-4 h-4 text-gray-400\" />\n                  <span className=\"text-gray-700 font-medium\">显示选项:</span>\n                </div>\n                \n                <label className=\"flex items-center space-x-2 cursor-pointer\">\n                  <input\n                    type=\"checkbox\"\n                    checked={showRealPrice}\n                    onChange={(e) => setShowRealPrice(e.target.checked)}\n                    className=\"rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n                  />\n                  <span>真实充值金额</span>\n                </label>\n                \n                <label className=\"flex items-center space-x-2 cursor-pointer\">\n                  <input\n                    type=\"checkbox\"\n                    checked={showRatioColumn}\n                    onChange={(e) => setShowRatioColumn(e.target.checked)}\n                    className=\"rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n                  />\n                  <span>显示倍率</span>\n                </label>\n                \n                <label className=\"flex items-center space-x-2 cursor-pointer\">\n                  <input\n                    type=\"checkbox\"\n                    checked={showEndpointTypes}\n                    onChange={(e) => setShowEndpointTypes(e.target.checked)}\n                    className=\"rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n                  />\n                  <span>端点类型</span>\n                </label>\n              </div>\n\n              {/* 统计信息 */}\n              <div className=\"flex items-center space-x-4 text-sm\">\n                <div className=\"flex items-center space-x-2 text-gray-600\">\n                  <CpuChipIcon className=\"w-4 h-4\" />\n                  <span>总计 <span className=\"font-medium text-gray-900\">{pricingData?.data?.length || 0}</span> 个模型</span>\n                </div>\n                <div className=\"h-4 w-px bg-gray-300\"></div>\n                <div className=\"text-blue-600\">\n                  <span>显示 <span className=\"font-medium\">{filteredModels.length}</span> 个</span>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          {/* 厂商 Tabs */}\n          <Tab.Group \n            selectedIndex={(() => {\n              const index = selectedProvider === 'all' ? 0 : Math.max(0, providers.indexOf(selectedProvider as ProviderType) + 1)\n              console.log('当前 selectedProvider:', selectedProvider, '计算的索引:', index)\n              return index\n            })()}\n            onChange={(index) => {\n              console.log('Tab 切换到索引:', index)\n              if (index === 0) {\n                console.log('切换到所有厂商')\n                setSelectedProvider('all')\n              } else {\n                const provider = providers[index - 1]\n                console.log('切换到厂商:', provider)\n                if (provider) {\n                  setSelectedProvider(provider)\n                }\n              }\n              // 滚动到选中的tab\n              setTimeout(() => scrollToSelectedTab(index), 50)\n            }}\n          >\n            <Tab.List \n              ref={tabListRef}\n              className=\"flex space-x-1 rounded-xl bg-gray-100 p-1 mb-6 overflow-x-auto overflow-y-hidden scrollbar-hide touch-pan-x\"\n            >\n              <Tab\n                className={({ selected }) =>\n                  `flex-shrink-0 rounded-lg py-2.5 px-4 text-sm font-medium leading-5 transition-all ${\n                    selected\n                      ? 'bg-white text-blue-700 shadow'\n                      : 'text-gray-700 hover:bg-white/60 hover:text-gray-900'\n                  }`\n                }\n              >\n                <div className=\"flex items-center justify-center space-x-2\">\n                  <CpuChipIcon className=\"w-4 h-4\" />\n                  <span>所有厂商 ({baseFilteredModels.length})</span>\n                </div>\n              </Tab>\n              {providers.map((provider) => {\n                // 直接使用 PROVIDER_CONFIGS 获取配置\n                const providerConfig = getProviderConfig(\n                  provider === 'OpenAI' ? 'gpt-4' :\n                  provider === 'Claude' ? 'claude-3' :\n                  provider === 'Gemini' ? 'gemini-pro' :\n                  provider === 'Grok' ? 'grok' :\n                  provider === 'Qwen' ? 'qwen' :\n                  provider === 'DeepSeek' ? 'deepseek' :\n                  'unknown'\n                )\n                const IconComponent = providerConfig.icon\n                return (\n                  <Tab\n                    key={provider}\n                    className={({ selected }) =>\n                      `flex-shrink-0 rounded-lg py-2.5 px-4 text-sm font-medium leading-5 transition-all ${\n                        selected\n                          ? 'bg-white text-blue-700 shadow'\n                          : 'text-gray-700 hover:bg-white/60 hover:text-gray-900'\n                      }`\n                    }\n                  >\n                    <div className=\"flex items-center justify-center space-x-2\">\n                      <IconComponent className=\"w-4 h-4\" />\n                      <span>{provider} ({getProviderFilteredCount(provider)})</span>\n                    </div>\n                  </Tab>\n                )\n              })}\n            </Tab.List>\n\n            <Tab.Panels>\n              {/* 所有厂商的面板 */}\n              <Tab.Panel>\n                <div className=\"space-y-3\">\n                  {filteredModels.length === 0 ? (\n                    <div className=\"text-center py-12\">\n                      <CpuChipIcon className=\"w-12 h-12 text-gray-300 mx-auto mb-4\" />\n                      <p className=\"text-gray-500\">没有找到匹配的模型</p>\n                    </div>\n                  ) : (\n                    filteredModels.map((item, index) => (\n                      <ModelItem\n                        key={`${item.model.model_name}-${index}`}\n                        model={item.model}\n                        calculatedPrice={item.calculatedPrice}\n                        exchangeRate={currentAccount?.balance?.USD > 0 ? currentAccount.balance.CNY / currentAccount.balance.USD : 7}\n                        showRealPrice={showRealPrice}\n                        showRatioColumn={showRatioColumn}\n                        showEndpointTypes={showEndpointTypes}\n                        userGroup={selectedGroup === 'all' ? 'default' : selectedGroup}\n                        onGroupClick={handleGroupClick}\n                        availableGroups={availableGroups}\n                        isAllGroupsMode={selectedGroup === 'all'}\n                      />\n                    ))\n                  )}\n                </div>\n              </Tab.Panel>\n              \n              {/* 为每个厂商创建对应的面板 */}\n              {providers.map((provider) => (\n                <Tab.Panel key={provider}>\n                  <div className=\"space-y-3\">\n                    {filteredModels.length === 0 ? (\n                      <div className=\"text-center py-12\">\n                        <CpuChipIcon className=\"w-12 h-12 text-gray-300 mx-auto mb-4\" />\n                        <p className=\"text-gray-500\">没有找到匹配的模型</p>\n                      </div>\n                    ) : (\n                      filteredModels.map((item, index) => (\n                        <ModelItem\n                          key={`${item.model.model_name}-${index}`}\n                          model={item.model}\n                          calculatedPrice={item.calculatedPrice}\n                          exchangeRate={currentAccount?.balance?.USD > 0 ? currentAccount.balance.CNY / currentAccount.balance.USD : 7}\n                          showRealPrice={showRealPrice}\n                          showRatioColumn={showRatioColumn}\n                          showEndpointTypes={showEndpointTypes}\n                          userGroup={selectedGroup === 'all' ? 'default' : selectedGroup}\n                          onGroupClick={handleGroupClick}\n                          availableGroups={availableGroups}\n                          isAllGroupsMode={selectedGroup === 'all'}\n                        />\n                      ))\n                    )}\n                  </div>\n                </Tab.Panel>\n              ))}\n            </Tab.Panels>\n          </Tab.Group>\n        </>\n      )}\n\n      {/* 说明文字 */}\n      {selectedAccount && pricingData && (\n        <div className=\"mt-8 p-4 bg-blue-50 border border-blue-200 rounded-lg\">\n          <div className=\"flex items-start space-x-3\">\n            <CpuChipIcon className=\"w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0\" />\n            <div className=\"text-sm\">\n              <p className=\"text-blue-800 font-medium mb-1\">模型定价说明</p>\n              <p className=\"text-blue-700\">\n                价格信息来源于站点提供的 API 接口，实际费用以各站点公布的价格为准。\n                按量计费模型的价格为每 1M tokens 的费用，按次计费模型显示每次调用的费用。\n              </p>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"one-api-hub\",\n  \"displayName\": \"中转站管理器 - One API Hub\",\n  \"version\": \"0.0.3\",\n  \"description\": \"一站式聚合管理所有AI中转站账号的余额、模型和密钥，告别繁琐登录。\",\n  \"author\": \"Plasmo Corp. <foss@plasmo.com>\",\n  \"scripts\": {\n    \"dev\": \"plasmo dev\",\n    \"build\": \"plasmo build\",\n    \"package\": \"plasmo package\"\n  },\n  \"dependencies\": {\n    \"@headlessui/react\": \"^2.2.4\",\n    \"@heroicons/react\": \"^2.2.0\", \n    \"@lobehub/icons\": \"^2.17.0\",\n    \"@plasmohq/storage\": \"^1.15.0\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"axios\": \"^1.10.0\",\n    \"dayjs\": \"^1.11.13\",\n    \"lodash-es\": \"^4.17.21\",\n    \"plasmo\": \"0.90.5\",\n    \"postcss\": \"^8.5.6\",\n    \"react\": \"18.2.0\",\n    \"react-countup\": \"^6.5.3\",\n    \"react-dom\": \"18.2.0\",\n    \"react-hot-toast\": \"^2.5.2\",\n    \"zustand\": \"^5.0.6\"\n  },\n  \"devDependencies\": {\n    \"@ianvs/prettier-plugin-sort-imports\": \"4.1.1\",\n    \"@tailwindcss/forms\": \"^0.5.10\",\n    \"@tailwindcss/typography\": \"^0.5.16\",\n    \"@types/chrome\": \"0.0.258\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"20.11.5\",\n    \"@types/react\": \"18.2.48\",\n    \"@types/react-dom\": \"18.2.18\",\n    \"prettier\": \"3.2.4\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"typescript\": \"5.3.3\"\n  },\n  \"manifest\": {\n    \"host_permissions\": [\n      \"https://*/*\"\n    ],\n    \"permissions\": [\n      \"tabs\",\n      \"storage\"\n    ]\n  }\n}\n"
  },
  {
    "path": "popup/index.tsx",
    "content": "import \"./style.css\"\nimport { useState, useCallback, useMemo, useEffect } from \"react\"\nimport toast, { Toaster } from 'react-hot-toast'\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport { calculateTotalConsumption, calculateTotalBalance, getOppositeCurrency } from \"../utils/formatters\"\nimport { useAccountData } from \"../hooks/useAccountData\"\nimport { useSort } from \"../hooks/useSort\"\nimport { useUserPreferences } from \"../hooks/useUserPreferences\"\nimport HeaderSection from \"../components/HeaderSection\"\nimport BalanceSection from \"../components/BalanceSection\"\nimport ActionButtons from \"../components/ActionButtons\"\nimport AccountList from \"../components/AccountList\"\nimport AddAccountDialog from \"../components/AddAccountDialog\"\nimport EditAccountDialog from \"../components/EditAccountDialog\"\nimport { accountStorage } from \"../services/accountStorage\"\nimport type { DisplaySiteData } from \"../types\"\n\nfunction IndexPopup() {\n  // 用户偏好设置管理\n  const {\n    preferences,\n    isLoading: preferencesLoading,\n    activeTab,\n    currencyType,\n    sortField,\n    sortOrder,\n    updateActiveTab,\n    updateCurrencyType,\n    updateSortConfig\n  } = useUserPreferences()\n\n  // 状态管理\n  const [isAddAccountOpen, setIsAddAccountOpen] = useState(false)\n  const [isEditAccountOpen, setIsEditAccountOpen] = useState(false)\n  const [editingAccount, setEditingAccount] = useState<DisplaySiteData | null>(null)\n  const [refreshingAccountId, setRefreshingAccountId] = useState<string | null>(null)\n\n  // 数据管理\n  const {\n    accounts,\n    displayData,\n    stats,\n    lastUpdateTime,\n    isInitialLoad,\n    isRefreshing,\n    prevTotalConsumption,\n    prevBalances,\n    loadAccountData,\n    handleRefresh\n  } = useAccountData()\n\n  // 排序管理 - 使用持久化的排序配置\n  const { sortField: currentSortField, sortOrder: currentSortOrder, sortedData, handleSort } = useSort(\n    displayData, \n    currencyType, \n    sortField, \n    sortOrder, \n    updateSortConfig\n  )\n\n  // 计算数据 - 使用 useMemo 缓存\n  const totalConsumption = useMemo(() => \n    calculateTotalConsumption(stats, accounts), \n    [stats, accounts]\n  )\n  \n  const totalBalance = useMemo(() => \n    calculateTotalBalance(displayData), \n    [displayData]\n  )\n  \n  const todayTokens = useMemo(() => ({\n    upload: stats.today_total_prompt_tokens,\n    download: stats.today_total_completion_tokens\n  }), [stats.today_total_prompt_tokens, stats.today_total_completion_tokens])\n\n  // 事件处理 - 使用 useCallback 优化\n  const handleCurrencyToggle = useCallback(async () => {\n    const newCurrency = getOppositeCurrency(currencyType)\n    await updateCurrencyType(newCurrency)\n  }, [currencyType, updateCurrencyType])\n\n  const handleTabChange = useCallback(async (index: number) => {\n    const newTab = index === 0 ? 'consumption' : 'balance'\n    await updateActiveTab(newTab)\n    console.log(`切换到${newTab === 'consumption' ? '今日消耗' : '总余额'}标签页`)\n  }, [updateActiveTab])\n\n  const handleOpenTab = useCallback(() => {\n    chrome.tabs.create({ url: chrome.runtime.getURL('options.html') })\n  }, [])\n\n  const handleOpenSettings = useCallback(() => {\n    chrome.tabs.create({ url: chrome.runtime.getURL('options.html#basic') })\n  }, [])\n\n  const handleAddAccount = useCallback(() => {\n    setIsAddAccountOpen(true)\n  }, [])\n\n  const handleCloseAddAccount = useCallback(() => {\n    setIsAddAccountOpen(false)\n    loadAccountData() // 重新加载数据\n  }, [loadAccountData])\n\n  const handleEditAccount = useCallback((account: DisplaySiteData) => {\n    setEditingAccount(account)\n    setIsEditAccountOpen(true)\n    console.log('编辑账号:', account.name)\n  }, [])\n\n  const handleCloseEditAccount = useCallback(() => {\n    setIsEditAccountOpen(false)\n    setEditingAccount(null)\n    loadAccountData() // 重新加载数据\n  }, [loadAccountData])\n\n  const handleDeleteAccount = useCallback((account: DisplaySiteData) => {\n    console.log('删除账号:', account.name)\n    loadAccountData() // 重新加载数据\n  }, [loadAccountData])\n\n  const handleGlobalRefresh = useCallback(async () => {\n    try {\n      await toast.promise(\n        handleRefresh(),\n        {\n          loading: '正在刷新所有账号...',\n          success: (result) => {\n            if (result.failed > 0) {\n              return `刷新完成：${result.success} 个成功，${result.failed} 个失败`\n            }\n            return '所有账号刷新成功!'\n          },\n          error: '刷新失败，请稍后重试',\n        }\n      )\n    } catch (error) {\n      console.error('刷新时出错:', error)\n    }\n  }, [handleRefresh])\n\n  const handleRefreshAccount = useCallback(async (account: DisplaySiteData) => {\n    if (refreshingAccountId) return // 防止重复刷新\n    \n    setRefreshingAccountId(account.id)\n    \n    const refreshPromise = async () => {\n      console.log('开始刷新账号:', account.name)\n      const success = await accountStorage.refreshAccount(account.id)\n      \n      if (success) {\n        console.log('账号刷新成功:', account.name)\n        // 刷新成功后重新加载数据，这将触发动画\n        await loadAccountData()\n        return success\n      } else {\n        console.warn('账号刷新失败:', account.name)\n        throw new Error('刷新失败')\n      }\n    }\n    \n    try {\n      await toast.promise(\n        refreshPromise(),\n        {\n          loading: `正在刷新 ${account.name}...`,\n          success: `${account.name} 刷新成功!`,\n          error: `${account.name} 刷新失败`,\n        }\n      )\n    } catch (error) {\n      console.error('刷新账号时出错:', error)\n    } finally {\n      setRefreshingAccountId(null)\n    }\n  }, [refreshingAccountId, loadAccountData])\n\n  const handleCopyUrl = useCallback((account: DisplaySiteData) => {\n    toast.success(`已复制 ${account.name} 的URL到剪贴板`)\n  }, [])\n\n  const handleViewKeys = useCallback((account: DisplaySiteData) => {\n    const url = chrome.runtime.getURL(`options.html#keys?accountId=${account.id}`)\n    chrome.tabs.create({ url })\n  }, [])\n\n  const handleViewKeysGlobal = useCallback(() => {\n    chrome.tabs.create({ url: chrome.runtime.getURL('options.html#keys') })\n  }, [])\n\n  const handleViewModels = useCallback((account: DisplaySiteData) => {\n    const url = chrome.runtime.getURL(`options.html#models?accountId=${account.id}`)\n    chrome.tabs.create({ url })\n  }, [])\n\n  const handleViewModelsGlobal = useCallback(() => {\n    chrome.tabs.create({ url: chrome.runtime.getURL('options.html#models') })\n  }, [])\n\n  const handleViewUsage = useCallback((account: DisplaySiteData) => {\n    const logUrl = `${account.baseUrl}/log`\n    chrome.tabs.create({ url: logUrl })\n  }, [])\n\n  // 处理打开插件时自动刷新\n  useEffect(() => {\n    let hasTriggered = false; // 防止重复触发\n    \n    const handleRefreshOnOpen = async () => {\n      // 等待偏好设置加载完成\n      if (preferencesLoading || !preferences || hasTriggered) {\n        return;\n      }\n\n      // 检查是否启用了打开插件时自动刷新\n      if (preferences.refreshOnOpen) {\n        hasTriggered = true;\n        console.log('[Popup] 打开插件时自动刷新已启用，开始刷新');\n        try {\n          await handleRefresh();\n          console.log('[Popup] 打开插件时自动刷新完成');\n        } catch (error) {\n          console.error('[Popup] 打开插件时自动刷新失败:', error);\n        }\n      }\n    };\n\n    handleRefreshOnOpen();\n  }, [preferencesLoading, preferences?.refreshOnOpen]); // 只依赖必要的属性\n\n  // 监听后台自动刷新的更新通知\n  useEffect(() => {\n    const handleBackgroundRefreshUpdate = (message: any) => {\n      if (message.type === 'AUTO_REFRESH_UPDATE') {\n        const { type, data } = message.payload;\n        \n        if (type === 'refresh_completed') {\n          console.log('[Popup] 后台刷新完成，重新加载数据');\n          loadAccountData(); // 重新加载数据以更新UI\n        } else if (type === 'refresh_error') {\n          console.error('[Popup] 后台刷新失败:', data.error);\n        }\n      }\n    };\n\n    chrome.runtime.onMessage.addListener(handleBackgroundRefreshUpdate);\n    \n    return () => {\n      chrome.runtime.onMessage.removeListener(handleBackgroundRefreshUpdate);\n    };\n  }, [loadAccountData]);\n\n  return (\n    <div className={`${UI_CONSTANTS.POPUP.WIDTH} bg-white flex flex-col ${UI_CONSTANTS.POPUP.HEIGHT}`}>\n      {/* 顶部导航栏 */}\n      <HeaderSection\n        isRefreshing={isRefreshing}\n        onRefresh={handleGlobalRefresh}\n        onOpenTab={handleOpenTab}\n        onOpenSettings={handleOpenSettings}\n      />\n\n      {/* 滚动内容区域 */}\n      <div className=\"flex-1 overflow-y-auto\">\n        {/* 基本信息展示 */}\n        {!preferencesLoading && (\n          <BalanceSection\n            totalConsumption={totalConsumption}\n            totalBalance={totalBalance}\n            todayTokens={todayTokens}\n            currencyType={currencyType}\n            activeTab={activeTab}\n            isInitialLoad={isInitialLoad}\n            lastUpdateTime={lastUpdateTime}\n            prevTotalConsumption={prevTotalConsumption}\n            onCurrencyToggle={handleCurrencyToggle}\n            onTabChange={handleTabChange}\n          />\n        )}\n\n        {/* 操作按钮组 */}\n        <ActionButtons \n          onAddAccount={handleAddAccount}\n          onViewKeys={handleViewKeysGlobal}\n          onViewModels={handleViewModelsGlobal}\n        />\n\n        {/* 站点账号列表 */}\n        <AccountList\n          sites={sortedData}\n          currencyType={currencyType}\n          sortField={currentSortField}\n          sortOrder={currentSortOrder}\n          isInitialLoad={isInitialLoad}\n          prevBalances={prevBalances}\n          refreshingAccountId={refreshingAccountId}\n          onSort={handleSort}\n          onAddAccount={handleAddAccount}\n          onRefreshAccount={handleRefreshAccount}\n          onCopyUrl={handleCopyUrl}\n          onViewKeys={handleViewKeys}\n          onViewModels={handleViewModels}\n          onViewUsage={handleViewUsage}\n          onEditAccount={handleEditAccount}\n          onDeleteAccount={handleDeleteAccount}\n        />\n      </div>\n\n      {/* 新增账号弹窗 */}\n      <AddAccountDialog \n        isOpen={isAddAccountOpen}\n        onClose={handleCloseAddAccount}\n      />\n      \n      {/* 编辑账号弹窗 */}\n      <EditAccountDialog \n        isOpen={isEditAccountOpen}\n        onClose={handleCloseEditAccount}\n        account={editingAccount}\n      />\n      \n      {/* Toast通知组件 */}\n      <Toaster\n        position=\"bottom-center\"\n        reverseOrder={true}\n      />\n    </div>\n  )\n}\n\nexport default IndexPopup\n"
  },
  {
    "path": "popup/style.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}"
  },
  {
    "path": "services/accountOperations.ts",
    "content": "/**\n * 账号操作服务模块\n * \n * 作用：\n * 1. 提供账号自动识别功能，通过 Chrome 扩展 API 获取站点用户信息\n * 2. 处理账号的验证、保存和更新操作\n * 3. 封装账号数据的存储逻辑，包括余额获取和数据同步\n * \n * 主要功能：\n * - autoDetectAccount: 自动识别站点账号信息（用户名、令牌、用户ID等）\n * - validateAndSaveAccount: 验证并保存新账号到本地存储\n * - validateAndUpdateAccount: 验证并更新现有账号信息\n * - extractDomainPrefix: 从域名提取站点名称\n * - isValidExchangeRate: 验证汇率输入的有效性\n * \n * 工作流程：\n * 1. 通过 background script 创建临时窗口访问目标站点\n * 2. 使用 content script 从站点获取用户信息\n * 3. 调用 API 获取访问令牌和账号数据\n * 4. 保存或更新账号信息到本地存储\n * \n * 依赖：\n * - accountStorage: 账号本地存储服务\n * - apiService: API 调用服务（获取令牌、余额等）\n * - autoDetectUtils: 错误处理工具\n */\n\nimport { accountStorage } from \"./accountStorage\"\nimport { fetchAccountData, getOrCreateAccessToken, fetchSiteStatus, extractDefaultExchangeRate } from \"./apiService\"\nimport { analyzeAutoDetectError, type AutoDetectError } from \"../utils/autoDetectUtils\"\nimport type { SiteAccount } from \"../types\"\n\n// 账号验证结果\nexport interface AccountValidationResult {\n  success: boolean\n  data?: {\n    username: string\n    accessToken: string\n    userId: string\n    exchangeRate?: number\n  }\n  error?: string\n  detailedError?: AutoDetectError\n}\n\n// 账号保存结果\nexport interface AccountSaveResult {\n  success: boolean\n  accountId?: string\n  error?: string\n}\n\n// 自动检测账号信息\nexport async function autoDetectAccount(url: string): Promise<AccountValidationResult> {\n  if (!url.trim()) {\n    return { success: false, error: '站点地址不能为空' }\n  }\n\n  try {\n    // 生成唯一的请求ID\n    const requestId = `auto-detect-${Date.now()}`\n    \n    // 尝试通过 background script 自动打开窗口并获取信息\n    const response = await chrome.runtime.sendMessage({\n      action: \"autoDetectSite\",\n      url: url.trim(),\n      requestId: requestId\n    })\n\n    if (!response.success) {\n      const detailedError = analyzeAutoDetectError(response.error || '自动检测失败')\n      return { \n        success: false, \n        error: response.error || '自动检测失败，请手动输入信息或确保已在目标站点登录',\n        detailedError\n      }\n    }\n\n    const userId = response.data.userId\n    if (!userId) {\n      const detailedError = analyzeAutoDetectError('无法获取用户 ID')\n      return { \n        success: false, \n        error: '无法获取用户 ID',\n        detailedError\n      }\n    }\n\n    // 并行执行：获取用户信息和站点状态\n    const [tokenInfo, siteStatus] = await Promise.all([\n      getOrCreateAccessToken(url, userId),\n      fetchSiteStatus(url.trim())\n    ])\n    \n    const { username: detectedUsername, access_token } = tokenInfo\n    \n    if (!detectedUsername || !access_token) {\n      const detailedError = analyzeAutoDetectError('未能获取到用户名或访问令牌')\n      return { \n        success: false, \n        error: '未能获取到用户名或访问令牌',\n        detailedError\n      }\n    }\n\n    // 获取默认充值比例\n    const defaultExchangeRate = extractDefaultExchangeRate(siteStatus)\n    \n    return {\n      success: true,\n      data: {\n        username: detectedUsername,\n        accessToken: access_token,\n        userId: userId.toString(),\n        exchangeRate: defaultExchangeRate\n      }\n    }\n  } catch (error) {\n    console.error('自动识别失败:', error)\n    const detailedError = analyzeAutoDetectError(error)\n    const errorMessage = error instanceof Error ? error.message : '未知错误'\n    return { \n      success: false, \n      error: `自动识别失败: ${errorMessage}`,\n      detailedError\n    }\n  }\n}\n\n// 验证并保存账号信息（用于新增）\nexport async function validateAndSaveAccount(\n  url: string,\n  siteName: string,\n  username: string,\n  accessToken: string,\n  userId: string,\n  exchangeRate: string\n): Promise<AccountSaveResult> {\n  // 表单验证\n  if (!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim()) {\n    return { success: false, error: '请填写完整的账号信息' }\n  }\n\n  const parsedUserId = parseInt(userId.trim())\n  if (isNaN(parsedUserId)) {\n    return { success: false, error: '用户 ID 必须是数字' }\n  }\n\n  try {\n    // 获取账号余额和今日使用情况\n    console.log('正在获取账号数据...')\n    const freshAccountData = await fetchAccountData(url.trim(), parsedUserId, accessToken.trim())\n\n    const accountData: Omit<SiteAccount, 'id' | 'created_at' | 'updated_at'> = {\n      emoji: \"\", // 不再使用 emoji\n      site_name: siteName.trim(),\n      site_url: url.trim(),\n      health_status: 'healthy', // 成功获取数据说明状态正常\n      exchange_rate: parseFloat(exchangeRate) || 7.2, // 使用用户输入的汇率\n      account_info: {\n        id: parsedUserId,\n        access_token: accessToken.trim(),\n        username: username.trim(),\n        quota: freshAccountData.quota,\n        today_prompt_tokens: freshAccountData.today_prompt_tokens,\n        today_completion_tokens: freshAccountData.today_completion_tokens,\n        today_quota_consumption: freshAccountData.today_quota_consumption,\n        today_requests_count: freshAccountData.today_requests_count\n      },\n      last_sync_time: Date.now()\n    }\n    \n    const accountId = await accountStorage.addAccount(accountData)\n    console.log('账号添加成功:', { \n      id: accountId, \n      siteName, \n      freshAccountData \n    })\n    \n    return { success: true, accountId }\n  } catch (error) {\n    console.error('保存账号失败:', error)\n    const errorMessage = error instanceof Error ? error.message : '未知错误'\n    return { success: false, error: `保存失败: ${errorMessage}` }\n  }\n}\n\n// 验证并更新账号信息（用于编辑）\nexport async function validateAndUpdateAccount(\n  accountId: string,\n  url: string,\n  siteName: string,\n  username: string,\n  accessToken: string,\n  userId: string,\n  exchangeRate: string\n): Promise<AccountSaveResult> {\n  // 表单验证\n  if (!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim()) {\n    return { success: false, error: '请填写完整的账号信息' }\n  }\n\n  const parsedUserId = parseInt(userId.trim())\n  if (isNaN(parsedUserId)) {\n    return { success: false, error: '用户 ID 必须是数字' }\n  }\n\n  try {\n    // 获取账号余额和今日使用情况\n    console.log('正在获取账号数据...')\n    const freshAccountData = await fetchAccountData(url.trim(), parsedUserId, accessToken.trim())\n\n    const updateData: Partial<Omit<SiteAccount, 'id' | 'created_at'>> = {\n      site_name: siteName.trim(),\n      site_url: url.trim(),\n      health_status: 'healthy', // 成功获取数据说明状态正常\n      exchange_rate: parseFloat(exchangeRate) || 7.2, // 使用用户输入的汇率\n      account_info: {\n        id: parsedUserId,\n        access_token: accessToken.trim(),\n        username: username.trim(),\n        quota: freshAccountData.quota,\n        today_prompt_tokens: freshAccountData.today_prompt_tokens,\n        today_completion_tokens: freshAccountData.today_completion_tokens,\n        today_quota_consumption: freshAccountData.today_quota_consumption,\n        today_requests_count: freshAccountData.today_requests_count\n      },\n      last_sync_time: Date.now()\n    }\n    \n    const success = await accountStorage.updateAccount(accountId, updateData)\n    if (!success) {\n      return { success: false, error: '更新账号失败' }\n    }\n    \n    console.log('账号更新成功:', { \n      id: accountId, \n      siteName, \n      freshAccountData \n    })\n    \n    return { success: true, accountId }\n  } catch (error) {\n    console.error('更新账号失败:', error)\n    const errorMessage = error instanceof Error ? error.message : '未知错误'\n    return { success: false, error: `更新失败: ${errorMessage}` }\n  }\n}\n\n// 提取域名的主要部分（一级域名前缀）\nexport function extractDomainPrefix(hostname: string): string {\n  if (!hostname) return \"\"\n  \n  // 移除 www. 前缀\n  const withoutWww = hostname.replace(/^www\\./, \"\")\n  \n  // 处理子域名情况，例如：xxx.xx.google.com -> google\n  const parts = withoutWww.split(\".\")\n  if (parts.length >= 2) {\n    // 如果是常见的二级域名（如 .com.cn, .co.uk 等），取倒数第三个部分\n    const lastPart = parts[parts.length - 1]\n    const secondLastPart = parts[parts.length - 2]\n    \n    // 检查是否为双重后缀\n    const doubleSuffixes = ['com', 'net', 'org', 'gov', 'edu', 'co']\n    if (parts.length >= 3 && doubleSuffixes.includes(secondLastPart) && lastPart.length === 2) {\n      // 首字母大写\n      return parts[parts.length - 3].charAt(0).toUpperCase() + parts[parts.length - 3].slice(1)\n    }\n    \n    // 否则返回倒数第二个部分\n    return secondLastPart.charAt(0).toUpperCase() + secondLastPart.slice(1)\n  }\n  \n  return withoutWww.charAt(0).toUpperCase() + withoutWww.slice(1)\n}\n\n// 验证充值比例是否有效\nexport function isValidExchangeRate(rate: string): boolean {\n  const num = parseFloat(rate)\n  return !isNaN(num) && num > 0 && num <= 100\n}"
  },
  {
    "path": "services/accountStorage.ts",
    "content": "import { Storage } from \"@plasmohq/storage\";\nimport { refreshAccountData } from './apiService';\nimport type { \n  SiteAccount, \n  StorageConfig, \n  AccountStats, \n  DisplaySiteData,\n  CurrencyType,\n  SiteHealthStatus \n} from \"../types\";\n\n// 存储键名常量\nconst STORAGE_KEYS = {\n  ACCOUNTS: 'site_accounts',\n  CONFIG: 'storage_config'\n} as const;\n\n// 默认配置\nconst DEFAULT_CONFIG: StorageConfig = {\n  accounts: [],\n  last_updated: Date.now()\n};\n\nclass AccountStorageService {\n  private storage: Storage;\n\n  constructor() {\n    this.storage = new Storage({\n      area: \"local\"\n    });\n  }\n\n  /**\n   * 获取所有账号信息\n   */\n  async getAllAccounts(): Promise<SiteAccount[]> {\n    try {\n      const config = await this.getStorageConfig();\n      return config.accounts;\n    } catch (error) {\n      console.error('获取账号信息失败:', error);\n      return [];\n    }\n  }\n\n  /**\n   * 根据 ID 获取单个账号信息\n   */\n  async getAccountById(id: string): Promise<SiteAccount | null> {\n    try {\n      const accounts = await this.getAllAccounts();\n      return accounts.find(account => account.id === id) || null;\n    } catch (error) {\n      console.error('获取账号信息失败:', error);\n      return null;\n    }\n  }\n\n  /**\n   * 添加新账号\n   */\n  async addAccount(accountData: Omit<SiteAccount, 'id' | 'created_at' | 'updated_at'>): Promise<string> {\n    try {\n      console.log('[AccountStorage] 开始添加新账号:', accountData.site_name);\n      const accounts = await this.getAllAccounts();\n      console.log('[AccountStorage] 当前账号数量:', accounts.length);\n      \n      const now = Date.now();\n      const newAccount: SiteAccount = {\n        ...accountData,\n        id: this.generateId(),\n        created_at: now,\n        updated_at: now\n      };\n\n      accounts.push(newAccount);\n      console.log('[AccountStorage] 准备保存账号，总数量:', accounts.length);\n      await this.saveAccounts(accounts);\n      console.log('[AccountStorage] 账号保存成功，ID:', newAccount.id);\n      \n      return newAccount.id;\n    } catch (error) {\n      console.error('[AccountStorage] 添加账号失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 更新账号信息\n   */\n  async updateAccount(id: string, updates: Partial<Omit<SiteAccount, 'id' | 'created_at'>>): Promise<boolean> {\n    try {\n      const accounts = await this.getAllAccounts();\n      const index = accounts.findIndex(account => account.id === id);\n      \n      if (index === -1) {\n        throw new Error(`账号 ${id} 不存在`);\n      }\n\n      accounts[index] = {\n        ...accounts[index],\n        ...updates,\n        updated_at: Date.now()\n      };\n\n      await this.saveAccounts(accounts);\n      return true;\n    } catch (error) {\n      console.error('更新账号失败:', error);\n      return false;\n    }\n  }\n\n  /**\n   * 删除账号\n   */\n  async deleteAccount(id: string): Promise<boolean> {\n    try {\n      const accounts = await this.getAllAccounts();\n      const filteredAccounts = accounts.filter(account => account.id !== id);\n      \n      if (filteredAccounts.length === accounts.length) {\n        console.error(`账号 ${id} 不存在，当前账号列表:`, accounts.map(acc => ({ id: acc.id, name: acc.site_name })));\n        throw new Error(`账号 ${id} 不存在`);\n      }\n\n      await this.saveAccounts(filteredAccounts);\n      return true;\n    } catch (error) {\n      console.error('删除账号失败:', error);\n      throw error; // 重新抛出错误，让调用者处理\n    }\n  }\n\n  /**\n   * 更新账号同步时间\n   */\n  async updateSyncTime(id: string): Promise<boolean> {\n    return this.updateAccount(id, { \n      last_sync_time: Date.now(),\n      updated_at: Date.now()\n    });\n  }\n\n  /**\n   * 刷新单个账号数据\n   */\n  async refreshAccount(id: string): Promise<boolean> {\n    try {\n      const account = await this.getAccountById(id);\n      if (!account) {\n        throw new Error(`账号 ${id} 不存在`);\n      }\n\n      // 使用同步导入的API服务\n      const result = await refreshAccountData(\n        account.site_url,\n        account.account_info.id,\n        account.account_info.access_token\n      );\n\n      // 构建更新数据\n      const updateData: Partial<Omit<SiteAccount, 'id' | 'created_at'>> = {\n        health_status: result.healthStatus.status,\n        last_sync_time: Date.now()\n      };\n\n      // 如果成功获取数据，更新账号信息\n      if (result.success && result.data) {\n        updateData.account_info = {\n          ...account.account_info,\n          quota: result.data.quota,\n          today_prompt_tokens: result.data.today_prompt_tokens,\n          today_completion_tokens: result.data.today_completion_tokens,\n          today_quota_consumption: result.data.today_quota_consumption,\n          today_requests_count: result.data.today_requests_count\n        };\n      }\n\n      // 更新账号信息\n      const updateSuccess = await this.updateAccount(id, updateData);\n      \n      // 记录健康状态变化\n      if (account.health_status !== result.healthStatus.status) {\n        console.log(`账号 ${account.site_name} 健康状态变化: ${account.health_status} -> ${result.healthStatus.status}`);\n        console.log(`状态详情: ${result.healthStatus.message}`);\n      }\n\n      return updateSuccess;\n    } catch (error) {\n      console.error('刷新账号数据失败:', error);\n      // 在出现异常时也尝试更新健康状态为unknown\n      try {\n        await this.updateAccount(id, {\n          health_status: 'unknown',\n          last_sync_time: Date.now()\n        });\n      } catch (updateError) {\n        console.error('更新健康状态失败:', updateError);\n      }\n      return false;\n    }\n  }\n\n  /**\n   * 刷新所有账号数据\n   */\n  async refreshAllAccounts(): Promise<{ success: number; failed: number }> {\n    const accounts = await this.getAllAccounts();\n    let success = 0;\n    let failed = 0;\n\n    // 使用 Promise.allSettled 来并发刷新，避免单个失败影响其他账号\n    const results = await Promise.allSettled(\n      accounts.map(account => this.refreshAccount(account.id))\n    );\n\n    results.forEach((result, index) => {\n      if (result.status === 'fulfilled' && result.value) {\n        success++;\n      } else {\n        failed++;\n        console.error(`刷新账号 ${accounts[index].site_name} 失败:`, \n          result.status === 'rejected' ? result.reason : '未知错误');\n      }\n    });\n\n    return { success, failed };\n  }\n\n  /**\n   * 计算账号统计信息\n   */\n  async getAccountStats(): Promise<AccountStats> {\n    try {\n      const accounts = await this.getAllAccounts();\n      \n      return accounts.reduce((stats, account) => ({\n        total_quota: stats.total_quota + account.account_info.quota,\n        today_total_consumption: stats.today_total_consumption + account.account_info.today_quota_consumption,\n        today_total_requests: stats.today_total_requests + account.account_info.today_requests_count,\n        today_total_prompt_tokens: stats.today_total_prompt_tokens + account.account_info.today_prompt_tokens,\n        today_total_completion_tokens: stats.today_total_completion_tokens + account.account_info.today_completion_tokens,\n      }), {\n        total_quota: 0,\n        today_total_consumption: 0,\n        today_total_requests: 0,\n        today_total_prompt_tokens: 0,\n        today_total_completion_tokens: 0,\n      });\n    } catch (error) {\n      console.error('计算统计信息失败:', error);\n      return {\n        total_quota: 0,\n        today_total_consumption: 0,\n        today_total_requests: 0,\n        today_total_prompt_tokens: 0,\n        today_total_completion_tokens: 0,\n      };\n    }\n  }\n\n  /**\n   * 转换为展示用的数据格式 (兼容当前 UI)\n   */\n  convertToDisplayData(accounts: SiteAccount[]): DisplaySiteData[] {\n    return accounts.map(account => ({\n      id: account.id,\n      icon: account.emoji,\n      name: account.site_name,\n      username: account.account_info.username,\n      balance: {\n        USD: parseFloat((account.account_info.quota / 500000).toFixed(2)),\n        CNY: parseFloat(((account.account_info.quota / 500000) * account.exchange_rate).toFixed(2))\n      },\n      todayConsumption: {\n        USD: parseFloat((account.account_info.today_quota_consumption / 500000).toFixed(2)),\n        CNY: parseFloat(((account.account_info.today_quota_consumption / 500000) * account.exchange_rate).toFixed(2))\n      },\n      todayTokens: {\n        upload: account.account_info.today_prompt_tokens,\n        download: account.account_info.today_completion_tokens\n      },\n      healthStatus: account.health_status,\n      baseUrl: account.site_url,\n      token: account.account_info.access_token,\n      userId: account.account_info.id // 添加真实的用户 ID\n    }));\n  }\n\n  /**\n   * 清空所有数据\n   */\n  async clearAllData(): Promise<boolean> {\n    try {\n      await this.storage.remove(STORAGE_KEYS.ACCOUNTS);\n      await this.storage.remove(STORAGE_KEYS.CONFIG);\n      return true;\n    } catch (error) {\n      console.error('清空数据失败:', error);\n      return false;\n    }\n  }\n\n  /**\n   * 导出数据\n   */\n  async exportData(): Promise<StorageConfig> {\n    return this.getStorageConfig();\n  }\n\n  /**\n   * 导入数据\n   */\n  async importData(data: StorageConfig): Promise<boolean> {\n    try {\n      await this.storage.set(STORAGE_KEYS.ACCOUNTS, {\n        ...data,\n        last_updated: Date.now()\n      });\n      return true;\n    } catch (error) {\n      console.error('导入数据失败:', error);\n      return false;\n    }\n  }\n\n  // 私有方法\n\n  /**\n   * 获取存储配置\n   */\n  private async getStorageConfig(): Promise<StorageConfig> {\n    try {\n      const config = await this.storage.get(STORAGE_KEYS.ACCOUNTS) as StorageConfig;\n      return config || DEFAULT_CONFIG;\n    } catch (error) {\n      console.error('获取存储配置失败:', error);\n      return DEFAULT_CONFIG;\n    }\n  }\n\n  /**\n   * 保存账号数据\n   */\n  private async saveAccounts(accounts: SiteAccount[]): Promise<void> {\n    console.log('[AccountStorage] 开始保存账号数据，数量:', accounts.length);\n    const config: StorageConfig = {\n      accounts,\n      last_updated: Date.now()\n    };\n    \n    console.log('[AccountStorage] 保存的配置数据:', { \n      accountCount: config.accounts.length,\n      last_updated: config.last_updated,\n      storageKey: STORAGE_KEYS.ACCOUNTS\n    });\n    \n    await this.storage.set(STORAGE_KEYS.ACCOUNTS, config);\n    console.log('[AccountStorage] 账号数据保存完成');\n  }\n\n  /**\n   * 生成唯一 ID\n   */\n  private generateId(): string {\n    return `account_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;\n  }\n}\n\n// 创建单例实例\nexport const accountStorage = new AccountStorageService();\n\n// 工具函数\nexport const AccountStorageUtils = {\n  /**\n   * 格式化余额显示\n   */\n  formatBalance(amount: number, currency: CurrencyType): string {\n    const symbol = currency === 'USD' ? '$' : '¥';\n    return `${symbol}${amount.toFixed(2)}`;\n  },\n\n  /**\n   * 格式化 token 数量\n   */\n  formatTokenCount(count: number): string {\n    if (count >= 1000000) {\n      return (count / 1000000).toFixed(1) + 'M';\n    } else if (count >= 1000) {\n      return (count / 1000).toFixed(1) + 'K';\n    }\n    return count.toString();\n  },\n\n  /**\n   * 验证账号数据\n   */\n  validateAccount(account: Partial<SiteAccount>): string[] {\n    const errors: string[] = [];\n\n    if (!account.site_name?.trim()) {\n      errors.push('站点名称不能为空');\n    }\n\n    if (!account.site_url?.trim()) {\n      errors.push('站点 URL 不能为空');\n    }\n\n    if (!account.account_info?.access_token?.trim()) {\n      errors.push('访问令牌不能为空');\n    }\n\n    if (!account.account_info?.username?.trim()) {\n      errors.push('用户名不能为空');\n    }\n\n    if (!account.health_status) {\n      errors.push('站点健康状态不能为空');\n    }\n\n    if (!account.exchange_rate || account.exchange_rate <= 0) {\n      errors.push('充值比例必须为正数');\n    }\n\n    return errors;\n  },\n\n  /**\n   * 生成默认 emoji（已禁用）\n   */\n  getRandomEmoji(): string {\n    return \"\"; // 不再使用 emoji\n  },\n\n  /**\n   * 获取健康状态的显示文本和样式\n   */\n  getHealthStatusInfo(status: SiteHealthStatus): { text: string; color: string; bgColor: string } {\n    switch (status) {\n      case 'healthy':\n        return { text: '正常', color: 'text-green-600', bgColor: 'bg-green-50' };\n      case 'warning':\n        return { text: '警告', color: 'text-yellow-600', bgColor: 'bg-yellow-50' };\n      case 'error':\n        return { text: '错误', color: 'text-red-600', bgColor: 'bg-red-50' };\n      case 'unknown':\n      default:\n        return { text: '未知', color: 'text-gray-500', bgColor: 'bg-gray-50' };\n    }\n  },\n\n  /**\n   * 检查账号是否需要刷新（基于最后同步时间）\n   */\n  isAccountStale(account: SiteAccount, maxAgeMinutes: number = 30): boolean {\n    const now = Date.now();\n    const ageMinutes = (now - account.last_sync_time) / (1000 * 60);\n    return ageMinutes > maxAgeMinutes;\n  },\n\n  /**\n   * 获取过期的账号列表\n   */\n  getStaleAccounts(accounts: SiteAccount[], maxAgeMinutes: number = 30): SiteAccount[] {\n    return accounts.filter(account => this.isAccountStale(account, maxAgeMinutes));\n  },\n\n  /**\n   * 批量验证账号信息\n   */\n  async validateAccounts(accounts: SiteAccount[]): Promise<{ valid: SiteAccount[]; invalid: SiteAccount[] }> {\n    const { validateAccountConnection } = await import('./apiService');\n    const valid: SiteAccount[] = [];\n    const invalid: SiteAccount[] = [];\n\n    const validationPromises = accounts.map(async (account) => {\n      try {\n        const isValid = await validateAccountConnection(\n          account.site_url,\n          account.account_info.id,\n          account.account_info.access_token\n        );\n        return { account, isValid };\n      } catch {\n        return { account, isValid: false };\n      }\n    });\n\n    const results = await Promise.allSettled(validationPromises);\n    \n    results.forEach((result, index) => {\n      const account = accounts[index];\n      if (result.status === 'fulfilled' && result.value.isValid) {\n        valid.push(account);\n      } else {\n        invalid.push(account);\n      }\n    });\n\n    return { valid, invalid };\n  }\n};"
  },
  {
    "path": "services/apiService.ts",
    "content": "/**\n * API 服务 - 用于与 One API/New API 站点进行交互\n */\n\n// ============= 类型定义 =============\nexport interface UserInfo {\n  id: number\n  username: string\n  access_token: string | null\n}\n\nexport interface AccessTokenInfo {\n  username: string\n  access_token: string\n}\n\nexport interface TodayUsageData {\n  today_quota_consumption: number\n  today_prompt_tokens: number\n  today_completion_tokens: number\n  today_requests_count: number\n}\n\nexport interface AccountData extends TodayUsageData {\n  quota: number\n}\n\nexport interface RefreshAccountResult {\n  success: boolean\n  data?: AccountData\n  healthStatus: HealthCheckResult\n}\n\nexport interface HealthCheckResult {\n  status: \"healthy\" | \"warning\" | \"error\" | \"unknown\"\n  message: string\n}\n\nexport interface SiteStatusInfo {\n  price?: number\n  stripe_unit_price?: number\n  PaymentUSDRate?: number\n}\n\n// 模型列表响应类型\nexport interface ModelsResponse {\n  data: string[]\n  message: string\n  success: boolean\n}\n\n// 分组信息类型\nexport interface GroupInfo {\n  desc: string\n  ratio: number\n}\n\n// 分组响应类型\nexport interface GroupsResponse {\n  data: Record<string, GroupInfo>\n  message: string\n  success: boolean\n}\n\n// 创建令牌请求类型\nexport interface CreateTokenRequest {\n  name: string\n  remain_quota: number\n  expired_time: number\n  unlimited_quota: boolean\n  model_limits_enabled: boolean\n  model_limits: string\n  allow_ips: string\n  group: string\n}\n\n// 创建令牌响应类型\nexport interface CreateTokenResponse {\n  message: string\n  success: boolean\n}\n\n// API令牌类型定义\nexport interface ApiToken {\n  id: number\n  user_id: number\n  key: string\n  status: number\n  name: string\n  created_time: number\n  accessed_time: number\n  expired_time: number\n  remain_quota: number\n  unlimited_quota: boolean\n  model_limits_enabled?: boolean\n  model_limits?: string\n  allow_ips?: string\n  used_quota: number\n  group?: string // 可选字段，某些站点可能没有\n  DeletedAt?: null\n  models?: string // 某些站点使用 models 而不是 model_limits\n}\n\n// 模型定价信息类型\nexport interface ModelPricing {\n  model_name: string\n  model_description?: string\n  quota_type: number // 0 = 按量计费，1 = 按次计费\n  model_ratio: number\n  model_price: number\n  owner_by?: string\n  completion_ratio: number\n  enable_groups: string[]\n  supported_endpoint_types: string[]\n}\n\n// 模型定价响应类型\nexport interface PricingResponse {\n  data: ModelPricing[]\n  group_ratio: Record<string, number>\n  success: boolean\n  usable_group: Record<string, string>\n}\n\n// 分页令牌响应类型\ninterface PaginatedTokenResponse {\n  page: number\n  page_size: number\n  total: number\n  items: ApiToken[]\n}\n\n// API 响应的通用格式\ninterface ApiResponse<T = any> {\n  success: boolean\n  data: T\n  message?: string\n}\n\n// 日志条目类型\ninterface LogItem {\n  quota?: number\n  prompt_tokens?: number\n  completion_tokens?: number\n}\n\n// 日志响应数据\ninterface LogResponseData {\n  items: LogItem[]\n  total: number\n}\n\n// ============= 常量定义 =============\nconst REQUEST_CONFIG = {\n  DEFAULT_PAGE_SIZE: 100,\n  MAX_PAGES: 100,\n  HEADERS: {\n    CONTENT_TYPE: \"application/json\",\n    PRAGMA: \"no-cache\"\n  }\n} as const\n\n// ============= 错误处理 =============\nexport class ApiError extends Error {\n  constructor(\n    message: string,\n    public statusCode?: number,\n    public endpoint?: string\n  ) {\n    super(message)\n    this.name = \"ApiError\"\n  }\n}\n\n// ============= 工具函数 =============\n/**\n * 创建请求头\n */\nconst createRequestHeaders = (\n  userId?: number,\n  accessToken?: string\n): Record<string, string> => {\n  const baseHeaders = {\n    \"Content-Type\": REQUEST_CONFIG.HEADERS.CONTENT_TYPE,\n    Pragma: REQUEST_CONFIG.HEADERS.PRAGMA\n  }\n\n  const userHeaders =\n    userId != null\n      ? {\n          \"New-API-User\": userId.toString(),\n          \"Veloera-User\": userId.toString()\n        }\n      : {}\n\n  const headers: Record<string, string> = { ...baseHeaders, ...userHeaders }\n\n  // TODO：bug，还是带上了 cookie，导致网站没有使用 access_token进行验证\n  if (accessToken) {\n    headers[\"Cookie\"] = \"\" // 使用 Bearer token 时清空 Cookie 头\n    headers[\"Authorization\"] = `Bearer ${accessToken}`\n  }\n\n  return headers\n}\n\n/**\n * 通用 API 请求处理器\n */\nconst apiRequest = async <T>(\n  url: string,\n  options: RequestInit,\n  endpoint: string\n): Promise<T> => {\n  const response = await fetch(url, options)\n\n  if (!response.ok) {\n    throw new ApiError(\n      `请求失败: ${response.status}`,\n      response.status,\n      endpoint\n    )\n  }\n\n  const data: ApiResponse<T> = await response.json()\n  if (!data.success || data.data === undefined) {\n    throw new ApiError(\"响应数据格式错误\", undefined, endpoint)\n  }\n\n  return data.data\n}\n\n/**\n * 创建带 cookie 认证的请求\n */\nconst createCookieAuthRequest = (userId?: number): RequestInit => ({\n  method: \"GET\",\n  headers: createRequestHeaders(userId),\n  credentials: \"include\"\n})\n\n/**\n * 创建带 Bearer token 认证的请求\n */\nconst createTokenAuthRequest = (\n  userId: number,\n  accessToken: string\n): RequestInit => ({\n  method: \"GET\",\n  headers: createRequestHeaders(userId, accessToken),\n  credentials: \"omit\" // 明确不携带 cookies\n})\n\n/**\n * 计算今日时间戳范围\n */\nconst getTodayTimestampRange = (): { start: number; end: number } => {\n  const today = new Date()\n\n  // 今日开始时间戳\n  today.setHours(0, 0, 0, 0)\n  const start = Math.floor(today.getTime() / 1000)\n\n  // 今日结束时间戳\n  today.setHours(23, 59, 59, 999)\n  const end = Math.floor(today.getTime() / 1000)\n\n  return { start, end }\n}\n\n/**\n * 聚合使用量数据\n */\nconst aggregateUsageData = (\n  items: LogItem[]\n): Omit<TodayUsageData, \"today_requests_count\"> => {\n  return items.reduce(\n    (acc, item) => ({\n      today_quota_consumption: acc.today_quota_consumption + (item.quota || 0),\n      today_prompt_tokens: acc.today_prompt_tokens + (item.prompt_tokens || 0),\n      today_completion_tokens:\n        acc.today_completion_tokens + (item.completion_tokens || 0)\n    }),\n    {\n      today_quota_consumption: 0,\n      today_prompt_tokens: 0,\n      today_completion_tokens: 0\n    }\n  )\n}\n\n// ============= 核心 API 函数 =============\n\n/**\n * 获取用户基本信息（用于账号检测） - 使用浏览器 cookie 认证\n */\nexport const fetchUserInfo = async (\n  baseUrl: string,\n  userId?: number\n): Promise<UserInfo> => {\n  const url = `${baseUrl}/api/user/self`\n  const options = createCookieAuthRequest(userId)\n\n  const userData = await apiRequest<UserInfo>(url, options, \"/api/user/self\")\n\n  return {\n    id: userData.id,\n    username: userData.username,\n    access_token: userData.access_token || null\n  }\n}\n\n/**\n * 创建访问令牌 - 使用浏览器 cookie 认证\n */\nexport const createAccessToken = async (\n  baseUrl: string,\n  userId: number\n): Promise<string> => {\n  const url = `${baseUrl}/api/user/token`\n  const options = createCookieAuthRequest(userId)\n\n  return await apiRequest<string>(url, options, \"/api/user/token\")\n}\n\n/**\n * 获取站点状态信息（包含充值比例）\n */\nexport const fetchSiteStatus = async (\n  baseUrl: string\n): Promise<SiteStatusInfo | null> => {\n  try {\n    const url = `${baseUrl}/api/status`\n    const options = {\n      method: \"GET\",\n      headers: {\n        \"Content-Type\": REQUEST_CONFIG.HEADERS.CONTENT_TYPE,\n        Pragma: REQUEST_CONFIG.HEADERS.PRAGMA\n      },\n      credentials: \"omit\" as RequestCredentials // 明确不携带 cookies\n    }\n\n    const response = await fetch(url, options)\n    if (!response.ok) {\n      console.warn(`获取站点状态失败: ${response.status}`)\n      return null\n    }\n\n    const data: ApiResponse<SiteStatusInfo> = await response.json()\n    if (!data.success || !data.data) {\n      console.warn(\"站点状态响应数据格式错误\")\n      return null\n    }\n\n    return data.data\n  } catch (error) {\n    console.warn(\"获取站点状态信息失败:\", error)\n    return null\n  }\n}\n\n/**\n * 从站点状态信息中提取默认充值比例\n */\nexport const extractDefaultExchangeRate = (\n  statusInfo: SiteStatusInfo | null\n): number | null => {\n  if (!statusInfo) {\n    return null\n  }\n\n  // 优先使用 price\n  if (statusInfo.price && statusInfo.price > 0) {\n    return statusInfo.price\n  }\n\n  // 次选 stripe_unit_price\n  if (statusInfo.stripe_unit_price && statusInfo.stripe_unit_price > 0) {\n    return statusInfo.stripe_unit_price\n  }\n\n  // 兼容 done-hub 和 one-hub\n  if (statusInfo.PaymentUSDRate && statusInfo.PaymentUSDRate > 0) {\n    return statusInfo.PaymentUSDRate\n  }\n  return null\n}\n\n/**\n * 自动获取或创建访问令牌\n */\nexport const getOrCreateAccessToken = async (\n  baseUrl: string,\n  userId: number\n): Promise<AccessTokenInfo> => {\n  // 首先获取用户信息\n  const userInfo = await fetchUserInfo(baseUrl, userId)\n\n  let accessToken = userInfo.access_token\n\n  // 如果没有访问令牌，则创建一个\n  if (!accessToken) {\n    console.log(\"访问令牌为空，尝试自动创建...\")\n    accessToken = await createAccessToken(baseUrl, userId)\n    console.log(\"自动创建访问令牌成功\")\n  }\n\n  return {\n    username: userInfo.username,\n    access_token: accessToken\n  }\n}\n\n/**\n * 获取账号余额信息\n */\nexport const fetchAccountQuota = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<number> => {\n  const url = `${baseUrl}/api/user/self`\n  const options = createTokenAuthRequest(userId, accessToken)\n\n  const userData = await apiRequest<{ quota?: number }>(\n    url,\n    options,\n    \"/api/user/self\"\n  )\n\n  return userData.quota || 0\n}\n\n/**\n * 获取今日使用情况\n */\nexport const fetchTodayUsage = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<TodayUsageData> => {\n  const { start: startTimestamp, end: endTimestamp } = getTodayTimestampRange()\n\n  let currentPage = 1\n  let totalRequestsCount = 0\n  let aggregatedData = {\n    today_quota_consumption: 0,\n    today_prompt_tokens: 0,\n    today_completion_tokens: 0\n  }\n\n  // 循环获取所有分页数据\n  while (currentPage <= REQUEST_CONFIG.MAX_PAGES) {\n    const params = new URLSearchParams({\n      p: currentPage.toString(),\n      page_size: REQUEST_CONFIG.DEFAULT_PAGE_SIZE.toString(),\n      type: \"0\",\n      token_name: \"\",\n      model_name: \"\",\n      start_timestamp: startTimestamp.toString(),\n      end_timestamp: endTimestamp.toString(),\n      group: \"\"\n    })\n\n    const url = `${baseUrl}/api/log/self?${params.toString()}`\n    const options = createTokenAuthRequest(userId, accessToken)\n\n    const logData = await apiRequest<LogResponseData>(\n      url,\n      options,\n      \"/api/log/self\"\n    )\n\n    const items = logData.items || []\n    const currentPageItemCount = items.length\n\n    // 聚合当前页数据\n    const pageData = aggregateUsageData(items)\n    aggregatedData.today_quota_consumption += pageData.today_quota_consumption\n    aggregatedData.today_prompt_tokens += pageData.today_prompt_tokens\n    aggregatedData.today_completion_tokens += pageData.today_completion_tokens\n\n    totalRequestsCount += currentPageItemCount\n\n    // 检查是否还有更多数据\n    const totalPages = Math.ceil(\n      (logData.total || 0) / REQUEST_CONFIG.DEFAULT_PAGE_SIZE\n    )\n    if (currentPage >= totalPages) {\n      break\n    }\n\n    currentPage++\n  }\n\n  if (currentPage > REQUEST_CONFIG.MAX_PAGES) {\n    console.warn(\n      `达到最大分页限制(${REQUEST_CONFIG.MAX_PAGES}页)，停止获取数据`\n    )\n  }\n\n  return {\n    ...aggregatedData,\n    today_requests_count: totalRequestsCount\n  }\n}\n\n/**\n * 获取完整的账号数据\n */\nexport const fetchAccountData = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<AccountData> => {\n  const [quota, todayUsage] = await Promise.all([\n    fetchAccountQuota(baseUrl, userId, accessToken),\n    fetchTodayUsage(baseUrl, userId, accessToken)\n  ])\n\n  return {\n    quota,\n    ...todayUsage\n  }\n}\n\n/**\n * 刷新单个账号数据\n */\nexport const refreshAccountData = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<RefreshAccountResult> => {\n  try {\n    const data = await fetchAccountData(baseUrl, userId, accessToken)\n    return {\n      success: true,\n      data,\n      healthStatus: {\n        status: \"healthy\",\n        message: \"账号状态正常\"\n      }\n    }\n  } catch (error) {\n    console.error(\"刷新账号数据失败:\", error)\n    return {\n      success: false,\n      healthStatus: determineHealthStatus(error)\n    }\n  }\n}\n\n/**\n * 验证账号连接性\n */\nexport const validateAccountConnection = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<boolean> => {\n  try {\n    await fetchAccountQuota(baseUrl, userId, accessToken)\n    return true\n  } catch (error) {\n    console.error(\"账号连接验证失败:\", error)\n    return false\n  }\n}\n\n/**\n * 获取账号令牌列表\n */\nexport const fetchAccountTokens = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string,\n  page: number = 0,\n  size: number = 100\n): Promise<ApiToken[]> => {\n  const params = new URLSearchParams({\n    p: page.toString(),\n    size: size.toString()\n  })\n\n  const url = `${baseUrl}/api/token/?${params.toString()}`\n  const options = createTokenAuthRequest(userId, accessToken)\n\n  try {\n    // 尝试获取响应数据，可能是直接的数组或者分页对象\n    const tokensData = await apiRequest<ApiToken[] | PaginatedTokenResponse>(\n      url,\n      options,\n      \"/api/token\"\n    )\n\n    // 处理不同的响应格式\n    if (Array.isArray(tokensData)) {\n      // 直接返回数组格式\n      return tokensData\n    } else if (\n      tokensData &&\n      typeof tokensData === \"object\" &&\n      \"items\" in tokensData\n    ) {\n      // 分页格式，返回 items 数组\n      return tokensData.items || []\n    } else {\n      // 其他情况，返回空数组\n      console.warn(\"Unexpected token response format:\", tokensData)\n      return []\n    }\n  } catch (error) {\n    console.error(\"获取令牌列表失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 获取可用模型列表\n */\nexport const fetchAvailableModels = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<string[]> => {\n  const url = `${baseUrl}/api/user/models`\n  const options = createTokenAuthRequest(userId, accessToken)\n\n  try {\n    const response = await apiRequest<string[]>(\n      url,\n      options,\n      \"/api/user/models\"\n    )\n    return response\n  } catch (error) {\n    console.error(\"获取模型列表失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 获取用户分组信息\n */\nexport const fetchUserGroups = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<Record<string, GroupInfo>> => {\n  const url = `${baseUrl}/api/user/self/groups`\n  const options = createTokenAuthRequest(userId, accessToken)\n\n  try {\n    const response = await apiRequest<Record<string, GroupInfo>>(\n      url,\n      options,\n      \"/api/user/self/groups\"\n    )\n    return response\n  } catch (error) {\n    console.error(\"获取分组信息失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 创建新的API令牌\n */\nexport const createApiToken = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string,\n  tokenData: CreateTokenRequest\n): Promise<boolean> => {\n  const url = `${baseUrl}/api/token/`\n  const options = {\n    method: \"POST\",\n    headers: createRequestHeaders(userId, accessToken),\n    credentials: \"omit\" as RequestCredentials,\n    body: JSON.stringify(tokenData)\n  }\n\n  try {\n    const response = await fetch(url, options)\n\n    if (!response.ok) {\n      throw new ApiError(\n        `请求失败: ${response.status}`,\n        response.status,\n        \"/api/token\"\n      )\n    }\n\n    const data: ApiResponse<any> = await response.json()\n\n    // 对于创建令牌的响应，只检查success字段，不要求data字段存在\n    if (!data.success) {\n      throw new ApiError(\n        data.message || \"创建令牌失败\",\n        undefined,\n        \"/api/token\"\n      )\n    }\n\n    return true\n  } catch (error) {\n    console.error(\"创建令牌失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 获取单个API令牌详情\n */\nexport const fetchTokenById = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string,\n  tokenId: number\n): Promise<ApiToken> => {\n  const url = `${baseUrl}/api/token/${tokenId}`\n  const options = createTokenAuthRequest(userId, accessToken)\n\n  try {\n    const response = await apiRequest<ApiToken>(\n      url,\n      options,\n      `/api/token/${tokenId}`\n    )\n    return response\n  } catch (error) {\n    console.error(\"获取令牌详情失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 更新API令牌\n */\nexport const updateApiToken = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string,\n  tokenId: number,\n  tokenData: CreateTokenRequest\n): Promise<boolean> => {\n  const url = `${baseUrl}/api/token/`\n  const options = {\n    method: \"PUT\",\n    headers: createRequestHeaders(userId, accessToken),\n    credentials: \"omit\" as RequestCredentials,\n    body: JSON.stringify({ ...tokenData, id: tokenId })\n  }\n\n  try {\n    const response = await fetch(url, options)\n\n    if (!response.ok) {\n      throw new ApiError(\n        `请求失败: ${response.status}`,\n        response.status,\n        \"/api/token\"\n      )\n    }\n\n    const data: ApiResponse<any> = await response.json()\n\n    if (!data.success) {\n      throw new ApiError(\n        data.message || \"更新令牌失败\",\n        undefined,\n        \"/api/token\"\n      )\n    }\n\n    return true\n  } catch (error) {\n    console.error(\"更新令牌失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 删除API令牌\n */\nexport const deleteApiToken = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string,\n  tokenId: number\n): Promise<boolean> => {\n  const url = `${baseUrl}/api/token/${tokenId}`\n  const options = {\n    method: \"DELETE\",\n    headers: createRequestHeaders(userId, accessToken),\n    credentials: \"omit\" as RequestCredentials\n  }\n\n  try {\n    const response = await fetch(url, options)\n\n    if (!response.ok) {\n      throw new ApiError(\n        `请求失败: ${response.status}`,\n        response.status,\n        `/api/token/${tokenId}`\n      )\n    }\n\n    const data: ApiResponse<any> = await response.json()\n\n    if (!data.success) {\n      throw new ApiError(\n        data.message || \"删除令牌失败\",\n        undefined,\n        `/api/token/${tokenId}`\n      )\n    }\n\n    return true\n  } catch (error) {\n    console.error(\"删除令牌失败:\", error)\n    throw error\n  }\n}\n\n/**\n * 获取模型定价信息\n */\nexport const fetchModelPricing = async (\n  baseUrl: string,\n  userId: number,\n  accessToken: string\n): Promise<PricingResponse> => {\n  const url = `${baseUrl}/api/pricing`\n  const options = createTokenAuthRequest(userId, accessToken)\n\n  try {\n    // /api/pricing 接口直接返回 PricingResponse 格式，不需要通过 apiRequest 包装\n    const response = await fetch(url, options)\n\n    if (!response.ok) {\n      throw new ApiError(\n        `请求失败: ${response.status}`,\n        response.status,\n        \"/api/pricing\"\n      )\n    }\n\n    const data: PricingResponse = await response.json()\n\n    if (!data.success) {\n      throw new ApiError(\"获取定价信息失败\", undefined, \"/api/pricing\")\n    }\n\n    return data\n  } catch (error) {\n    console.error(\"获取模型定价失败:\", error)\n    throw error\n  }\n}\n\n// ============= 健康状态判断 =============\n\n/**\n * 根据错误判断健康状态\n */\nexport const determineHealthStatus = (error: any): HealthCheckResult => {\n  if (error instanceof ApiError) {\n    // HTTP响应码不为200的情况\n    if (error.statusCode) {\n      return {\n        status: \"warning\",\n        message: `HTTP ${error.statusCode}: ${error.message}`\n      }\n    }\n    // 其他API错误（数据格式错误等）\n    return {\n      status: \"unknown\",\n      message: error.message\n    }\n  }\n\n  // 网络连接失败、超时等HTTP请求失败的情况\n  if (error instanceof TypeError && error.message.includes(\"fetch\")) {\n    return {\n      status: \"error\",\n      message: \"网络连接失败\"\n    }\n  }\n\n  // 其他未知错误\n  return {\n    status: \"unknown\",\n    message: error?.message || \"未知错误\"\n  }\n}\n"
  },
  {
    "path": "services/autoRefreshService.ts",
    "content": "import { userPreferences } from './userPreferences';\nimport { accountStorage } from './accountStorage';\n\n/**\n * 自动刷新服务\n * 负责管理后台定时刷新功能\n */\nclass AutoRefreshService {\n  private refreshTimer: NodeJS.Timeout | null = null;\n  private isInitialized = false;\n\n  /**\n   * 初始化自动刷新服务\n   */\n  async initialize() {\n    if (this.isInitialized) {\n      console.log('[AutoRefresh] 服务已初始化');\n      return;\n    }\n\n    try {\n      await this.setupAutoRefresh();\n      this.isInitialized = true;\n      console.log('[AutoRefresh] 服务初始化成功');\n    } catch (error) {\n      console.error('[AutoRefresh] 服务初始化失败:', error);\n    }\n  }\n\n  /**\n   * 根据用户设置启动或停止自动刷新\n   */\n  async setupAutoRefresh() {\n    try {\n      // 清除现有定时器\n      if (this.refreshTimer) {\n        clearInterval(this.refreshTimer);\n        this.refreshTimer = null;\n        console.log('[AutoRefresh] 已清除现有定时器');\n      }\n\n      // 获取用户偏好设置\n      const preferences = await userPreferences.getPreferences();\n      \n      if (!preferences.autoRefresh) {\n        console.log('[AutoRefresh] 自动刷新已关闭');\n        return;\n      }\n\n      // 启动定时刷新\n      const intervalMs = preferences.refreshInterval * 1000;\n      this.refreshTimer = setInterval(async () => {\n        await this.performBackgroundRefresh();\n      }, intervalMs);\n\n      console.log(`[AutoRefresh] 自动刷新已启动，间隔: ${preferences.refreshInterval}秒`);\n    } catch (error) {\n      console.error('[AutoRefresh] 设置自动刷新失败:', error);\n    }\n  }\n\n  /**\n   * 执行后台刷新\n   */\n  private async performBackgroundRefresh() {\n    try {\n      console.log('[AutoRefresh] 开始执行后台刷新');\n      \n      // 直接调用accountStorage的刷新方法\n      const result = await accountStorage.refreshAllAccounts();\n      console.log(`[AutoRefresh] 后台刷新完成 - 成功: ${result.success}, 失败: ${result.failed}`);\n\n      // 通知前端更新（如果popup是打开的）\n      this.notifyFrontend('refresh_completed', result);\n    } catch (error) {\n      console.error('[AutoRefresh] 后台刷新失败:', error);\n      this.notifyFrontend('refresh_error', { error: error.message });\n    }\n  }\n\n  /**\n   * 立即执行一次刷新\n   */\n  async refreshNow(): Promise<{ success: number; failed: number }> {\n    try {\n      console.log('[AutoRefresh] 执行立即刷新');\n      const result = await accountStorage.refreshAllAccounts();\n      console.log(`[AutoRefresh] 立即刷新完成 - 成功: ${result.success}, 失败: ${result.failed}`);\n      return result;\n    } catch (error) {\n      console.error('[AutoRefresh] 立即刷新失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 停止自动刷新\n   */\n  stopAutoRefresh() {\n    if (this.refreshTimer) {\n      clearInterval(this.refreshTimer);\n      this.refreshTimer = null;\n      console.log('[AutoRefresh] 自动刷新已停止');\n    }\n  }\n\n  /**\n   * 更新刷新设置\n   */\n  async updateSettings(settings: {\n    autoRefresh?: boolean;\n    refreshInterval?: number;\n  }) {\n    try {\n      await userPreferences.updateAutoRefreshSettings(settings);\n      await this.setupAutoRefresh(); // 重新设置定时器\n      console.log('[AutoRefresh] 设置已更新:', settings);\n    } catch (error) {\n      console.error('[AutoRefresh] 更新设置失败:', error);\n    }\n  }\n\n  /**\n   * 获取当前状态\n   */\n  getStatus() {\n    return {\n      isRunning: this.refreshTimer !== null,\n      isInitialized: this.isInitialized\n    };\n  }\n\n  /**\n   * 通知前端\n   */\n  private notifyFrontend(type: string, data: any) {\n    try {\n      // 向所有连接的客户端发送消息\n      chrome.runtime.sendMessage({\n        type: 'AUTO_REFRESH_UPDATE',\n        payload: { type, data }\n      }).catch((error) => {\n        // 静默处理\"没有接收者\"的错误（popup可能没打开）\n        if (error.message.includes('receiving end does not exist')) {\n          console.log('[AutoRefresh] 前端未打开，跳过通知');\n        } else {\n          console.warn('[AutoRefresh] 通知前端失败:', error);\n        }\n      });\n    } catch (error) {\n      // 静默处理错误，避免影响后台刷新\n      console.warn('[AutoRefresh] 发送消息异常，可能前端未打开');\n    }\n  }\n\n  /**\n   * 销毁服务\n   */\n  destroy() {\n    this.stopAutoRefresh();\n    this.isInitialized = false;\n    console.log('[AutoRefresh] 服务已销毁');\n  }\n}\n\n// 创建单例实例\nexport const autoRefreshService = new AutoRefreshService();\n\n// 消息处理器\nexport const handleAutoRefreshMessage = async (request: any, sendResponse: Function) => {\n  try {\n    switch (request.action) {\n      case 'setupAutoRefresh':\n        await autoRefreshService.setupAutoRefresh();\n        sendResponse({ success: true });\n        break;\n      \n      case 'refreshNow':\n        const result = await autoRefreshService.refreshNow();\n        sendResponse({ success: true, data: result });\n        break;\n      \n      case 'stopAutoRefresh':\n        autoRefreshService.stopAutoRefresh();\n        sendResponse({ success: true });\n        break;\n      \n      case 'updateAutoRefreshSettings':\n        await autoRefreshService.updateSettings(request.settings);\n        sendResponse({ success: true });\n        break;\n      \n      case 'getAutoRefreshStatus':\n        const status = autoRefreshService.getStatus();\n        sendResponse({ success: true, data: status });\n        break;\n      \n      default:\n        sendResponse({ success: false, error: '未知的操作' });\n    }\n  } catch (error) {\n    console.error('[AutoRefresh] 处理消息失败:', error);\n    sendResponse({ success: false, error: error.message });\n  }\n};"
  },
  {
    "path": "services/userPreferences.ts",
    "content": "import { Storage } from \"@plasmohq/storage\";\n\n// 用户偏好设置类型定义\nexport interface UserPreferences {\n  // BalanceSection 相关配置\n  activeTab: 'consumption' | 'balance';  // 金额标签页状态\n  currencyType: 'USD' | 'CNY';           // 金额单位\n\n  // AccountList 相关配置\n  sortField: 'name' | 'balance' | 'consumption';  // 排序字段\n  sortOrder: 'asc' | 'desc';                      // 排序顺序\n\n  // 自动刷新相关配置\n  autoRefresh: boolean;                  // 是否启用定时自动刷新\n  refreshInterval: number;               // 刷新间隔（秒）\n  refreshOnOpen: boolean;                // 打开插件时自动刷新\n  showHealthStatus: boolean;             // 是否显示健康状态\n\n  // 其他配置可在此扩展\n  lastUpdated: number;  // 最后更新时间\n}\n\n// 存储键名常量\nconst STORAGE_KEYS = {\n  USER_PREFERENCES: 'user_preferences'\n} as const;\n\n// 默认配置\nconst DEFAULT_PREFERENCES: UserPreferences = {\n  activeTab: 'consumption',\n  currencyType: 'USD',\n  sortField: 'balance',  // 与 UI_CONSTANTS.SORT.DEFAULT_FIELD 保持一致\n  sortOrder: 'desc',     // 与 UI_CONSTANTS.SORT.DEFAULT_ORDER 保持一致\n  autoRefresh: true,     // 默认启用自动刷新\n  refreshInterval: 360,  // 默认360秒刷新间隔\n  refreshOnOpen: true,   // 默认打开插件时自动刷新\n  showHealthStatus: true,// 默认显示健康状态\n  lastUpdated: Date.now()\n};\n\nclass UserPreferencesService {\n  private storage: Storage;\n\n  constructor() {\n    this.storage = new Storage({\n      area: \"local\"\n    });\n  }\n\n  /**\n   * 获取用户偏好设置\n   */\n  async getPreferences(): Promise<UserPreferences> {\n    try {\n      const preferences = await this.storage.get(STORAGE_KEYS.USER_PREFERENCES) as UserPreferences;\n      return preferences || DEFAULT_PREFERENCES;\n    } catch (error) {\n      console.error('获取用户偏好设置失败:', error);\n      return DEFAULT_PREFERENCES;\n    }\n  }\n\n  /**\n   * 保存用户偏好设置\n   */\n  async savePreferences(preferences: Partial<UserPreferences>): Promise<boolean> {\n    try {\n      const currentPreferences = await this.getPreferences();\n      const updatedPreferences: UserPreferences = {\n        ...currentPreferences,\n        ...preferences,\n        lastUpdated: Date.now()\n      };\n\n      await this.storage.set(STORAGE_KEYS.USER_PREFERENCES, updatedPreferences);\n      console.log('[UserPreferences] 偏好设置保存成功:', updatedPreferences);\n      return true;\n    } catch (error) {\n      console.error('[UserPreferences] 保存偏好设置失败:', error);\n      return false;\n    }\n  }\n\n  /**\n   * 更新活动标签页\n   */\n  async updateActiveTab(activeTab: 'consumption' | 'balance'): Promise<boolean> {\n    return this.savePreferences({ activeTab });\n  }\n\n  /**\n   * 更新货币类型\n   */\n  async updateCurrencyType(currencyType: 'USD' | 'CNY'): Promise<boolean> {\n    return this.savePreferences({ currencyType });\n  }\n\n  /**\n   * 更新排序配置\n   */\n  async updateSortConfig(sortField: 'name' | 'balance' | 'consumption', sortOrder: 'asc' | 'desc'): Promise<boolean> {\n    return this.savePreferences({ sortField, sortOrder });\n  }\n\n  /**\n   * 更新自动刷新设置\n   */\n  async updateAutoRefreshSettings(settings: {\n    autoRefresh?: boolean;\n    refreshInterval?: number;\n    refreshOnOpen?: boolean;\n    showHealthStatus?: boolean;\n  }): Promise<boolean> {\n    return this.savePreferences(settings);\n  }\n\n  /**\n   * 更新自动刷新开关\n   */\n  async updateAutoRefresh(autoRefresh: boolean): Promise<boolean> {\n    return this.savePreferences({ autoRefresh });\n  }\n\n  /**\n   * 更新刷新间隔\n   */\n  async updateRefreshInterval(refreshInterval: number): Promise<boolean> {\n    return this.savePreferences({ refreshInterval });\n  }\n\n  /**\n   * 更新打开插件时自动刷新设置\n   */\n  async updateRefreshOnOpen(refreshOnOpen: boolean): Promise<boolean> {\n    return this.savePreferences({ refreshOnOpen });\n  }\n\n  /**\n   * 更新健康状态显示设置\n   */\n  async updateShowHealthStatus(showHealthStatus: boolean): Promise<boolean> {\n    return this.savePreferences({ showHealthStatus });\n  }\n\n  /**\n   * 重置为默认设置\n   */\n  async resetToDefaults(): Promise<boolean> {\n    try {\n      await this.storage.set(STORAGE_KEYS.USER_PREFERENCES, DEFAULT_PREFERENCES);\n      console.log('[UserPreferences] 已重置为默认设置');\n      return true;\n    } catch (error) {\n      console.error('[UserPreferences] 重置设置失败:', error);\n      return false;\n    }\n  }\n\n  /**\n   * 清空偏好设置\n   */\n  async clearPreferences(): Promise<boolean> {\n    try {\n      await this.storage.remove(STORAGE_KEYS.USER_PREFERENCES);\n      console.log('[UserPreferences] 偏好设置已清空');\n      return true;\n    } catch (error) {\n      console.error('[UserPreferences] 清空偏好设置失败:', error);\n      return false;\n    }\n  }\n\n  /**\n   * 导出偏好设置\n   */\n  async exportPreferences(): Promise<UserPreferences> {\n    return this.getPreferences();\n  }\n\n  /**\n   * 导入偏好设置\n   */\n  async importPreferences(preferences: UserPreferences): Promise<boolean> {\n    try {\n      await this.storage.set(STORAGE_KEYS.USER_PREFERENCES, {\n        ...preferences,\n        lastUpdated: Date.now()\n      });\n      console.log('[UserPreferences] 偏好设置导入成功');\n      return true;\n    } catch (error) {\n      console.error('[UserPreferences] 导入偏好设置失败:', error);\n      return false;\n    }\n  }\n}\n\n// 创建单例实例\nexport const userPreferences = new UserPreferencesService();\n\n// 工具函数\nexport const UserPreferencesUtils = {\n  /**\n   * 验证偏好设置数据\n   */\n  validatePreferences(preferences: Partial<UserPreferences>): string[] {\n    const errors: string[] = [];\n\n    if (preferences.activeTab && !['consumption', 'balance'].includes(preferences.activeTab)) {\n      errors.push('activeTab 必须是 \"consumption\" 或 \"balance\"');\n    }\n\n    if (preferences.currencyType && !['USD', 'CNY'].includes(preferences.currencyType)) {\n      errors.push('currencyType 必须是 \"USD\" 或 \"CNY\"');\n    }\n\n    if (preferences.sortField && !['name', 'balance', 'consumption'].includes(preferences.sortField)) {\n      errors.push('sortField 必须是 \"name\", \"balance\" 或 \"consumption\"');\n    }\n\n    if (preferences.sortOrder && !['asc', 'desc'].includes(preferences.sortOrder)) {\n      errors.push('sortOrder 必须是 \"asc\" 或 \"desc\"');\n    }\n\n    if (preferences.autoRefresh !== undefined && typeof preferences.autoRefresh !== 'boolean') {\n      errors.push('autoRefresh 必须是布尔值');\n    }\n\n    if (preferences.refreshInterval !== undefined) {\n      if (typeof preferences.refreshInterval !== 'number' || preferences.refreshInterval < 10) {\n        errors.push('refreshInterval 必须大于等于10秒');\n      }\n    }\n\n    if (preferences.refreshOnOpen !== undefined && typeof preferences.refreshOnOpen !== 'boolean') {\n      errors.push('refreshOnOpen 必须是布尔值');\n    }\n\n    if (preferences.showHealthStatus !== undefined && typeof preferences.showHealthStatus !== 'boolean') {\n      errors.push('showHealthStatus 必须是布尔值');\n    }\n\n    return errors;\n  },\n\n  /**\n   * 获取标签页的显示名称\n   */\n  getTabDisplayName(tab: 'consumption' | 'balance'): string {\n    return tab === 'consumption' ? '今日消耗' : '总余额';\n  },\n\n  /**\n   * 获取货币类型的显示符号\n   */\n  getCurrencySymbol(currency: 'USD' | 'CNY'): string {\n    return currency === 'USD' ? '$' : '¥';\n  },\n\n  /**\n   * 获取排序字段的显示名称\n   */\n  getSortFieldDisplayName(field: 'name' | 'balance' | 'consumption'): string {\n    switch (field) {\n      case 'name': return '账号名称';\n      case 'balance': return '余额';\n      case 'consumption': return '今日消耗';\n      default: return '未知';\n    }\n  },\n\n  /**\n   * 获取排序顺序的显示名称\n   */\n  getSortOrderDisplayName(order: 'asc' | 'desc'): string {\n    return order === 'asc' ? '升序' : '降序';\n  }\n};"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"!./node_modules\", \"./**/*.{js,ts,jsx,tsx}\"],\n  theme: {\n    extend: {}\n  },\n  plugins: [\n    require(\"@tailwindcss/forms\"),\n    require(\"@tailwindcss/typography\"),\n    function ({ addUtilities }) {\n      const newUtilities = {\n        \".scrollbar-hide\": {\n          /* IE and Edge */\n          \"-ms-overflow-style\": \"none\",\n          /* Firefox */\n          \"scrollbar-width\": \"none\",\n          /* Safari and Chrome */\n          \"&::-webkit-scrollbar\": {\n            display: \"none\"\n          }\n        }\n      }\n      addUtilities(newUtilities)\n    }\n  ]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"plasmo/templates/tsconfig.base\",\n  \"exclude\": [\n    \"node_modules\"\n  ],\n  \"include\": [\n    \".plasmo/index.d.ts\",\n    \"./**/*.ts\",\n    \"./**/*.tsx\",\n    \"./global.d.ts\"\n  ],\n  \"compilerOptions\": {\n    \"paths\": {\n      \"~*\": [\n        \"./*\"\n      ]\n    },\n    \"baseUrl\": \".\"\n  }\n}\n"
  },
  {
    "path": "types/index.ts",
    "content": "// 账号信息数据类型定义\n\n// 站点健康状态\nexport type SiteHealthStatus = 'healthy' | 'warning' | 'error' | 'unknown';\n\n// 账号基础信息\nexport interface AccountInfo {\n  id: number; // 账号 ID（整数）\n  access_token: string;\n  username: string;\n  quota: number; // 总余额点数\n  today_prompt_tokens: number; // 今日 prompt_tokens\n  today_completion_tokens: number; // 今日 completion_tokens\n  today_quota_consumption: number; // 今日消耗 quota\n  today_requests_count: number; // 今日请求次数\n}\n\n// 站点账号完整信息\nexport interface SiteAccount {\n  id: string; // 此项 id\n  emoji: string; // 此项 emoji\n  site_name: string; // 站点名称\n  site_url: string; // 站点 url\n  health_status: SiteHealthStatus; // 站点健康状态\n  exchange_rate: number; // 人民币与美元充值比例 (CNY per USD)\n  account_info: AccountInfo; // 账号信息\n  last_sync_time: number; // 最后同步时间 (timestamp)\n  updated_at: number; // 更改时间 (timestamp)\n  created_at: number; // 创建时间 (timestamp)\n}\n\n// 存储配置\nexport interface StorageConfig {\n  accounts: SiteAccount[];\n  last_updated: number;\n}\n\n// 账号统计信息 (用于展示)\nexport interface AccountStats {\n  total_quota: number;\n  today_total_consumption: number;\n  today_total_requests: number;\n  today_total_prompt_tokens: number;\n  today_total_completion_tokens: number;\n}\n\n// API 响应相关类型\nexport interface ApiResponse<T = any> {\n  success: boolean;\n  data?: T;\n  message?: string;\n}\n\n// 用于排序的字段类型\nexport type SortField = 'site_name' | 'quota' | 'today_quota_consumption' | 'last_sync_time';\nexport type SortOrder = 'asc' | 'desc';\n\n// 货币类型\nexport type CurrencyType = 'USD' | 'CNY';\n\n// 展示用的站点数据 (兼容当前 UI)\nexport interface DisplaySiteData {\n  id: string;\n  icon: string;\n  name: string;\n  username: string;\n  balance: { USD: number; CNY: number };\n  todayConsumption: { USD: number; CNY: number };\n  todayTokens: { upload: number; download: number };\n  healthStatus?: SiteHealthStatus; // 可选的健康状态\n  baseUrl: string; // 站点 URL，用于复制功能\n  token: string; // 访问令牌，用于复制功能\n  userId: number; // 真实的用户 ID，用于 API 调用\n}"
  },
  {
    "path": "utils/autoDetectUtils.ts",
    "content": "/**\n * 自动识别错误处理工具模块\n * \n * 作用：\n * 1. 定义自动识别过程中可能出现的错误类型枚举\n * 2. 提供智能错误分析功能，将通用错误信息转换为结构化的错误对象\n * 3. 包含错误处理的辅助函数，如打开登录页面等\n * \n * 主要功能：\n * - analyzeAutoDetectError: 分析错误消息并返回结构化错误信息\n * - getLoginUrl: 生成站点登录页面URL\n * - openLoginTab: 在新标签页中打开登录页面\n * \n * 使用场景：\n * - AddAccountDialog 和 EditAccountDialog 中的自动识别错误处理\n * - AutoDetectErrorAlert 组件中的错误展示和操作\n */\n\n// 自动识别错误类型\nexport enum AutoDetectErrorType {\n  TIMEOUT = 'timeout',\n  UNAUTHORIZED = 'unauthorized', \n  INVALID_RESPONSE = 'invalid_response',\n  NETWORK_ERROR = 'network_error',\n  UNKNOWN = 'unknown'\n}\n\n// 自动识别错误信息\nexport interface AutoDetectError {\n  type: AutoDetectErrorType\n  message: string\n  actionText?: string\n  actionUrl?: string\n  helpDocUrl?: string\n}\n\n// 分析错误并返回结构化错误信息\nexport function analyzeAutoDetectError(error: string | Error): AutoDetectError {\n  const errorMessage = error instanceof Error ? error.message : error\n  \n  // 超时错误\n  if (errorMessage.includes('超时') || errorMessage.includes('timeout')) {\n    return {\n      type: AutoDetectErrorType.TIMEOUT,\n      message: '自动识别超时，请尝试手动添加',\n      helpDocUrl: 'https://fxaxg.github.io/one-api-hub/faq.html' \n    }\n  }\n  \n  // 401 认证错误\n  if (errorMessage.includes('401') || errorMessage.includes('未授权') || errorMessage.includes('Unauthorized')) {\n    return {\n      type: AutoDetectErrorType.UNAUTHORIZED,\n      message: '您未在当前站点登录，或者登录信息已过期，无法自动添加，可查看帮助文档或点击先进行登录',\n      actionText: '登录此站点',\n      helpDocUrl: 'https://fxaxg.github.io/one-api-hub/faq.html' \n    }\n  }\n  \n  // 响应格式错误\n  if (errorMessage.includes('格式') || errorMessage.includes('解析') || errorMessage.includes('JSON') || \n      errorMessage.includes('数据不符合') || errorMessage.includes('无法获取')) {\n    return {\n      type: AutoDetectErrorType.INVALID_RESPONSE,\n      message: '自动识别未成功，站点返回数据不符合预期，请手动输入信息或确保已在目标站点登录',\n      helpDocUrl: 'https://fxaxg.github.io/one-api-hub/faq.html' \n    }\n  }\n  \n  // 网络错误\n  if (errorMessage.includes('网络') || errorMessage.includes('连接') || errorMessage.includes('Network')) {\n    return {\n      type: AutoDetectErrorType.NETWORK_ERROR,\n      message: '网络连接失败，请检查网络连接后重试',\n      helpDocUrl: 'https://fxaxg.github.io/one-api-hub/faq.html' \n    }\n  }\n  \n  // 未知错误\n  return {\n    type: AutoDetectErrorType.UNKNOWN,\n    message: '自动识别失败：' + errorMessage,\n    helpDocUrl: 'https://fxaxg.github.io/one-api-hub/faq.html' \n  }\n}\n\n// 创建错误消息组件的props\nexport interface AutoDetectErrorProps {\n  error: AutoDetectError\n  siteUrl?: string\n  onHelpClick?: () => void\n  onActionClick?: () => void\n}\n\n// 获取用于打开登录页面的URL\nexport function getLoginUrl(siteUrl: string): string {\n  try {\n    const url = new URL(siteUrl)\n    // 对于 One API 和 New API，通常登录页面在 /login\n    return `${url.protocol}//${url.host}/login`\n  } catch {\n    return siteUrl\n  }\n}\n\n// 打开新标签页进行登录\nexport function openLoginTab(siteUrl: string): void {\n  const loginUrl = getLoginUrl(siteUrl)\n  chrome.tabs.create({ url: loginUrl })\n}"
  },
  {
    "path": "utils/formatters.ts",
    "content": "import dayjs from \"dayjs\"\nimport relativeTime from \"dayjs/plugin/relativeTime\"\nimport \"dayjs/locale/zh-cn\"\nimport { UI_CONSTANTS, CURRENCY_SYMBOLS } from \"../constants/ui\"\nimport type { DisplaySiteData, AccountStats } from \"../types\"\n\n// 初始化 dayjs\ndayjs.extend(relativeTime)\ndayjs.locale('zh-cn')\n\n/**\n * 格式化 Token 数量\n */\nexport const formatTokenCount = (count: number): string => {\n  if (count >= UI_CONSTANTS.TOKEN.MILLION_THRESHOLD) {\n    return (count / UI_CONSTANTS.TOKEN.MILLION_THRESHOLD).toFixed(1) + 'M'\n  } else if (count >= UI_CONSTANTS.TOKEN.THOUSAND_THRESHOLD) {\n    return (count / UI_CONSTANTS.TOKEN.THOUSAND_THRESHOLD).toFixed(1) + 'K'\n  }\n  return count.toString()\n}\n\n/**\n * 格式化相对时间\n */\nexport const formatRelativeTime = (date: Date): string => {\n  const now = dayjs()\n  const targetTime = dayjs(date)\n  const diffInSeconds = now.diff(targetTime, 'second')\n  \n  if (diffInSeconds < 5) {\n    return '刚刚'\n  }\n  return targetTime.fromNow()\n}\n\n/**\n * 格式化具体时间\n */\nexport const formatFullTime = (date: Date): string => {\n  return dayjs(date).format('YYYY/MM/DD HH:mm:ss')\n}\n\n/**\n * 计算总消耗\n */\nexport const calculateTotalConsumption = (\n  stats: AccountStats,\n  accounts: any[]\n) => {\n  const usdAmount = parseFloat((stats.today_total_consumption / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR).toFixed(2))\n  const cnyAmount = parseFloat(accounts.reduce((sum, acc) => \n    sum + ((acc.account_info.today_quota_consumption / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR) * acc.exchange_rate), 0\n  ).toFixed(2))\n\n  return {\n    USD: usdAmount,\n    CNY: cnyAmount\n  }\n}\n\n/**\n * 计算总余额\n */\nexport const calculateTotalBalance = (\n  displayData: DisplaySiteData[]\n) => {\n  return {\n    USD: parseFloat(displayData.reduce((sum, site) => sum + site.balance.USD, 0).toFixed(2)),\n    CNY: parseFloat(displayData.reduce((sum, site) => sum + site.balance.CNY, 0).toFixed(2))\n  }\n}\n\n/**\n * 获取货币符号\n */\nexport const getCurrencySymbol = (currencyType: 'USD' | 'CNY'): string => {\n  return CURRENCY_SYMBOLS[currencyType]\n}\n\n/**\n * 获取货币显示名称\n */\nexport const getCurrencyDisplayName = (currencyType: 'USD' | 'CNY'): string => {\n  return currencyType === 'USD' ? '美元' : '人民币'\n}\n\n/**\n * 获取切换后的货币类型\n */\nexport const getOppositeCurrency = (currencyType: 'USD' | 'CNY'): 'USD' | 'CNY' => {\n  return currencyType === 'USD' ? 'CNY' : 'USD'\n}\n\n/**\n * 生成排序比较函数\n */\nexport const createSortComparator = <T>(\n  field: keyof T,\n  order: 'asc' | 'desc'\n) => {\n  return (a: T, b: T): number => {\n    const aValue = a[field]\n    const bValue = b[field]\n    \n    if (order === 'asc') {\n      return aValue < bValue ? -1 : aValue > bValue ? 1 : 0\n    } else {\n      return aValue > bValue ? -1 : aValue < bValue ? 1 : 0\n    }\n  }\n}\n\n/**\n * 生成唯一ID\n */\nexport const generateId = (prefix = 'id'): string => {\n  return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`\n}\n\n/**\n * 防抖函数\n */\nexport const debounce = <T extends (...args: any[]) => any>(\n  func: T,\n  wait: number\n): (...args: Parameters<T>) => void => {\n  let timeout: NodeJS.Timeout | null = null\n  \n  return (...args: Parameters<T>) => {\n    if (timeout) {\n      clearTimeout(timeout)\n    }\n    timeout = setTimeout(() => func(...args), wait)\n  }\n}\n\n/**\n * 节流函数\n */\nexport const throttle = <T extends (...args: any[]) => any>(\n  func: T,\n  limit: number\n): (...args: Parameters<T>) => void => {\n  let inThrottle = false\n  \n  return (...args: Parameters<T>) => {\n    if (!inThrottle) {\n      func(...args)\n      inThrottle = true\n      setTimeout(() => (inThrottle = false), limit)\n    }\n  }\n}"
  },
  {
    "path": "utils/modelPricing.ts",
    "content": "/**\n * 模型定价计算工具\n */\n\nimport type { ModelPricing } from '../services/apiService'\n\nexport interface CalculatedPrice {\n  inputUSD: number  // 每1M token输入价格（美元）\n  outputUSD: number // 每1M token输出价格（美元）\n  inputCNY: number  // 每1M token输入价格（人民币）\n  outputCNY: number // 每1M token输出价格（人民币）\n  perCallPrice?: number // 按次计费时每次调用的价格\n}\n\n/**\n * 计算模型价格\n * @param model 模型定价信息\n * @param groupRatio 分组倍率\n * @param exchangeRate 汇率（CNY per USD）\n * @param userGroup 用户分组\n */\nexport const calculateModelPrice = (\n  model: ModelPricing,\n  groupRatio: Record<string, number>,\n  exchangeRate: number,\n  userGroup: string = 'default'\n): CalculatedPrice => {\n  // 获取用户分组的倍率，默认为1\n  const groupMultiplier = groupRatio[userGroup] || 1\n\n  if (model.quota_type === 0) {\n    // 按量计费\n    // inputUSD（每 1M token） = model_ratio × 2 × groupRatio\n    // complUSD（每 1M token） = model_ratio × completion_ratio × 2 × groupRatio\n    const inputUSD = model.model_ratio * 2 * groupMultiplier\n    const outputUSD = model.model_ratio * model.completion_ratio * 2 * groupMultiplier\n    \n    return {\n      inputUSD,\n      outputUSD,\n      inputCNY: inputUSD * exchangeRate,\n      outputCNY: outputUSD * exchangeRate\n    }\n  } else {\n    // 按次计费\n    const perCallPrice = model.model_price * groupMultiplier\n    \n    return {\n      inputUSD: 0,\n      outputUSD: 0,\n      inputCNY: 0,\n      outputCNY: 0,\n      perCallPrice\n    }\n  }\n}\n\n/**\n * 格式化价格显示\n */\nexport const formatPrice = (\n  price: number, \n  currency: 'USD' | 'CNY' = 'USD', \n  precision: number = 4\n): string => {\n  const symbol = currency === 'USD' ? '$' : '¥'\n  \n  if (price === 0) return `${symbol}0`\n  \n  if (price < 0.0001) {\n    return `${symbol}${price.toExponential(2)}`\n  }\n  \n  return `${symbol}${price.toFixed(precision)}`\n}\n\n/**\n * 格式化价格显示 - 简洁格式\n */\nexport const formatPriceCompact = (\n  price: number, \n  currency: 'USD' | 'CNY' = 'USD'\n): string => {\n  const symbol = currency === 'USD' ? '$' : '¥'\n  \n  if (price === 0) return `${symbol}0`\n  \n  if (price < 0.01) {\n    return `${symbol}${price.toFixed(6)}`\n  } else if (price < 1) {\n    return `${symbol}${price.toFixed(4)}`\n  } else {\n    return `${symbol}${price.toFixed(2)}`\n  }\n}\n\n/**\n * 格式化价格区间显示（输入-输出）\n */\nexport const formatPriceRange = (\n  inputPrice: number,\n  outputPrice: number,\n  currency: 'USD' | 'CNY' = 'USD',\n  precision: number = 4\n): string => {\n  const formattedInput = formatPrice(inputPrice, currency, precision)\n  const formattedOutput = formatPrice(outputPrice, currency, precision)\n  \n  if (inputPrice === outputPrice) {\n    return formattedInput\n  }\n  \n  return `${formattedInput} ~ ${formattedOutput}`\n}\n\n/**\n * 获取计费模式的显示文本\n */\nexport const getBillingModeText = (quotaType: number): string => {\n  return quotaType === 0 ? '按量计费' : '按次计费'\n}\n\n/**\n * 获取计费模式的样式\n */\nexport const getBillingModeStyle = (quotaType: number): { color: string; bgColor: string } => {\n  return quotaType === 0 \n    ? { color: 'text-blue-600', bgColor: 'bg-blue-50' }\n    : { color: 'text-purple-600', bgColor: 'bg-purple-50' }\n}\n\n/**\n * 检查模型是否对指定分组可用\n */\nexport const isModelAvailableForGroup = (model: ModelPricing, userGroup: string): boolean => {\n  return model.enable_groups.includes(userGroup)\n}\n\n/**\n * 获取模型的可用端点类型显示文本\n */\nexport const getEndpointTypesText = (endpointTypes: string[] | undefined): string => {\n  if (!endpointTypes || !Array.isArray(endpointTypes)) {\n    return '未提供'\n  }\n  return endpointTypes.join(', ')\n}"
  },
  {
    "path": "utils/modelProviders.ts",
    "content": "/**\n * 模型厂商识别和图标映射工具\n */\n\nimport { OpenAI, Claude, Gemini, Grok, Qwen, DeepSeek } from '@lobehub/icons'\n\n// 厂商类型\nexport type ProviderType = 'OpenAI' | 'Claude' | 'Gemini' | 'Grok' | 'Qwen' | 'DeepSeek' | 'Unknown'\n\n// 厂商配置接口\nexport interface ProviderConfig {\n  name: string\n  icon: React.ComponentType<any>\n  patterns: RegExp[]\n  color: string\n  bgColor: string\n}\n\n// 厂商配置映射\nexport const PROVIDER_CONFIGS: Record<ProviderType, ProviderConfig> = {\n  OpenAI: {\n    name: 'OpenAI',\n    icon: OpenAI,\n    patterns: [\n      /gpt/i,\n      /o\\d+/i, // o1, o3 等\n      /text-embedding/i\n    ],\n    color: 'text-green-600',\n    bgColor: 'bg-green-50'\n  },\n  Claude: {\n    name: 'Claude',\n    icon: Claude,\n    patterns: [\n      /claude/i,\n      /sonnet/i,\n      /haiku/i,\n      /neptune/i,\n      /opus/i\n    ],\n    color: 'text-orange-600',\n    bgColor: 'bg-orange-50'\n  },\n  Gemini: {\n    name: 'Gemini',\n    icon: Gemini,\n    patterns: [\n      /gemini/i\n    ],\n    color: 'text-blue-600',\n    bgColor: 'bg-blue-50'\n  },\n  Grok: {\n    name: 'Grok',\n    icon: Grok,\n    patterns: [\n      /grok/i\n    ],\n    color: 'text-gray-900',\n    bgColor: 'bg-gray-50'\n  },\n  Qwen: {\n    name: '阿里',\n    icon: Qwen,\n    patterns: [\n      /qwen/i\n    ],\n    color: 'text-purple-600',\n    bgColor: 'bg-purple-50'\n  },\n  DeepSeek: {\n    name: 'DeepSeek',\n    icon: DeepSeek,\n    patterns: [\n      /deepseek/i\n    ],\n    color: 'text-cyan-600',\n    bgColor: 'bg-cyan-50'\n  },\n  Unknown: {\n    name: 'Unknown',\n    icon: () => null,\n    patterns: [],\n    color: 'text-gray-600',\n    bgColor: 'bg-gray-50'\n  }\n}\n\n/**\n * 根据模型名称识别厂商\n */\nexport const identifyProvider = (modelName: string): ProviderType => {\n  for (const [providerType, config] of Object.entries(PROVIDER_CONFIGS)) {\n    if (providerType === 'Unknown') continue\n    \n    for (const pattern of config.patterns) {\n      if (pattern.test(modelName)) {\n        return providerType as ProviderType\n      }\n    }\n  }\n  \n  return 'Unknown'\n}\n\n/**\n * 获取厂商配置\n */\nexport const getProviderConfig = (modelName: string): ProviderConfig => {\n  const providerType = identifyProvider(modelName)\n  return PROVIDER_CONFIGS[providerType]\n}\n\n/**\n * 获取所有厂商类型\n */\nexport const getAllProviders = (): ProviderType[] => {\n  return Object.keys(PROVIDER_CONFIGS).filter(key => key !== 'Unknown') as ProviderType[]\n}\n\n/**\n * 根据厂商类型过滤模型\n */\nexport const filterModelsByProvider = <T extends { model_name: string }>(\n  models: T[], \n  providerType: ProviderType | 'all'\n): T[] => {\n  if (providerType === 'all') return models\n  \n  return models.filter(model => identifyProvider(model.model_name) === providerType)\n}"
  }
]