Full Code of fxaxg/one-api-hub for AI

main a8a2ac83a3af cached
56 files
328.8 KB
85.6k tokens
170 symbols
1 requests
Download .txt
Showing preview only (373K chars total). Download the full file or copy to clipboard to get everything.
Repository: fxaxg/one-api-hub
Branch: main
Commit: a8a2ac83a3af
Files: 56
Total size: 328.8 KB

Directory structure:
gitextract_sbci5x21/

├── .github/
│   └── workflows/
│       ├── deploy-docs.yml
│       └── submit.yml
├── .gitignore
├── .prettierrc.mjs
├── CLAUDE.md
├── LICENSE
├── README.md
├── background.ts
├── components/
│   ├── AccountList.tsx
│   ├── ActionButtons.tsx
│   ├── AddAccountDialog.tsx
│   ├── AddTokenDialog.tsx
│   ├── AutoDetectErrorAlert.tsx
│   ├── BalanceSection.tsx
│   ├── CopyKeyDialog.tsx
│   ├── DelAccountDialog.tsx
│   ├── EditAccountDialog.tsx
│   ├── HeaderSection.tsx
│   ├── ModelItem.tsx
│   └── Tooltip.tsx
├── constants/
│   └── ui.ts
├── content.ts
├── debug/
│   └── testStorage.ts
├── docs/
│   ├── docs/
│   │   ├── .vuepress/
│   │   │   └── config.js
│   │   ├── README.md
│   │   ├── faq.md
│   │   └── get-started.md
│   └── package.json
├── examples/
│   └── storageExample.ts
├── global.d.ts
├── hooks/
│   ├── useAccountData.ts
│   ├── useSort.ts
│   ├── useTimeFormatter.ts
│   └── useUserPreferences.ts
├── options/
│   ├── index.tsx
│   └── pages/
│       ├── About.tsx
│       ├── BasicSettings.tsx
│       ├── ImportExport.tsx
│       ├── KeyManagement.tsx
│       └── ModelList.tsx
├── package.json
├── popup/
│   ├── index.tsx
│   └── style.css
├── postcss.config.js
├── services/
│   ├── accountOperations.ts
│   ├── accountStorage.ts
│   ├── apiService.ts
│   ├── autoRefreshService.ts
│   └── userPreferences.ts
├── tailwind.config.js
├── tsconfig.json
├── types/
│   └── index.ts
└── utils/
    ├── autoDetectUtils.ts
    ├── formatters.ts
    ├── modelPricing.ts
    └── modelProviders.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/deploy-docs.yml
================================================
name: 部署文档

on:
  push:
    branches:
      # 确保这是你正在使用的分支名称
      - dev
    paths: # <<<<<< 新增:路径过滤
      - 'docs/docs/**'          # 匹配 /docs/docs/ 目录下所有文档内容和配置 (包括 .vuepress/、config.js 等)
      - 'docs/package.json'     # 匹配 /docs/package.json 文件 (因为依赖定义在这里)
      - 'docs/pnpm-lock.json'   # 匹配 /docs/pnpm-lock.json 文件 (因为依赖版本锁定在这里)

permissions:
  contents: write

jobs:
  deploy-gh-pages:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: docs # 所有 run 命令将在此目录下执行

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          # 如果你的文档需要 Git 子模块,取消注释下一行
          # submodules: true

      - name: 安装 pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 8

      - name: 设置 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: 安装依赖
        run: pnpm install --no-frozen-lockfile

      - name: 构建文档
        env:
          NODE_OPTIONS: --max_old_space_size=8192
        run: |-
          # 因为 working-directory 是 docs,所以这里执行的是 docs/package.json 中的 docs:build
          # 且构建输出在 /docs/docs/.vuepress/dist (相对于仓库根目录)
          # 或者在当前工作目录 /docs 内的 docs/.vuepress/dist
          pnpm run docs:build
          # 创建 .nojekyll 文件,防止 GitHub Pages 尝试解析 Jekyll
          # 该文件将位于 /docs/docs/.vuepress/dist/.nojekyll
          > docs/.vuepress/dist/.nojekyll

      - name: 部署文档
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          branch: gh-pages # 部署到的目标分支
          # 指定要部署的文件夹。相对于 GitHub 仓库根目录。
          # 您的 VitePress 文档内容在 /docs/docs/ 下,构建输出通常在 /docs/docs/.vuepress/dist
          folder: docs/docs/.vuepress/dist

================================================
FILE: .github/workflows/submit.yml
================================================
name: "Submit to Web Store"
on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Cache pnpm modules
        uses: actions/cache@v3
        with:
          path: ~/.pnpm-store
          key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-
      - uses: pnpm/action-setup@v2.2.4
        with:
          version: latest
          run_install: true
      - name: Use Node.js 16.x
        uses: actions/setup-node@v3.4.1
        with:
          node-version: 16.x
          cache: "pnpm"
      - name: Build the extension
        run: pnpm build
      - name: Package the extension into a zip artifact
        run: pnpm package
      - name: Browser Platform Publish
        uses: PlasmoHQ/bpp@v3
        with:
          keys: ${{ secrets.SUBMIT_KEYS }}
          artifact: build/chrome-mv3-prod.zip


================================================
FILE: .gitignore
================================================

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

out/
build/
dist/

# plasmo
.plasmo

# typescript
.tsbuildinfo

.idea

# 文档项目的依赖和构建产物
/docs/node_modules/
/docs/docs/.vuepress/dist/
/docs/docs/.vuepress/.cache/
/docs/docs/.vuepress/.temp/

# 其他可能的文档生成器特定忽略项 (例如 Docusaurus 的 build 目录)
# /docs/build/

================================================
FILE: .prettierrc.mjs
================================================
/**
 * @type {import('prettier').Options}
 */
export default {
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  semi: false,
  singleQuote: false,
  trailingComma: "none",
  bracketSpacing: true,
  bracketSameLine: true,
  plugins: ["@ianvs/prettier-plugin-sort-imports"],
  importOrder: [
    "<BUILTIN_MODULES>", // Node.js built-in modules
    "<THIRD_PARTY_MODULES>", // Imports not matched by other special words or groups.
    "", // Empty line
    "^@plasmo/(.*)$",
    "",
    "^@plasmohq/(.*)$",
    "",
    "^~(.*)$",
    "",
    "^[./]"
  ]
}


================================================
FILE: CLAUDE.md
================================================
## 用户定义
- 当前项目使用 Plasmo v0.90.5 开发
- 前端技术栈(Tailwind CSS v3、Headless UI等)
- 当技术文档不确定时,应当使用 mcp 工具 context7 进行搜索

### 项目介绍
```
## 介绍

目前市面上有太多 ai-api 中转站点,每次查看余额和支持模型列表等信息都非常麻烦,需要逐个登录查看。

本插件可以便捷的对基于https://github.com/songquanpeng/one-api和[new-api](https://github.com/QuantumNous/new-api)等部署的 Ai 中转站账号进行整合管理。

### 功能

- 自动识别中转站点,自动创建系统访问 token 并添加到插件的站点列表中
- 每个站点可添加多个账号
- 账号的余额、使用日志进行查看
- 令牌(key)查看与管理
- 站点支持模型信息和渠道查看
- 插件无需联网

### 未来支持

- 模型降智测试
- webdav 数据备份
```


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2025 fxaxg

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: README.md
================================================
<div align="center">
  <img src="assets/icon.png" alt="One API Hub Logo" width="128" height="128">
  
  # 中转站管理器 - One API Hub
  
  **一个开源的浏览器插件,聚合管理所有中转站账号的余额、模型和密钥,告别繁琐登录。**
  
  [![Version](https://img.shields.io/badge/version-0.0.3-blue.svg)](https://github.com/fxaxg/one-api-hub)
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
  [![Plasmo](https://img.shields.io/badge/plasmo-v0.90.5-purple.svg)](https://plasmo.com)
  [![React](https://img.shields.io/badge/react-18.2.0-61dafb.svg)](https://reactjs.org)
  [![TypeScript](https://img.shields.io/badge/typescript-5.3.3-blue.svg)](https://typescriptlang.org)
  [![Tailwind CSS](https://img.shields.io/badge/tailwindcss-3.4.17-38bdf8.svg)](https://tailwindcss.com)

   **[文档教程](https://fxaxg.github.io/one-api-hub/) | [常见问题](https://fxaxg.github.io/one-api-hub/faq.html)**
  
</div>

---

## 📖 介绍

目前市面上有太多 AI-API 中转站点,每次查看余额和支持模型列表等信息都非常麻烦,需要逐个登录查看。

本插件可以便捷的对基于以下项目的AI 中转站账号进行整合管理:
- [one-api](https://github.com/songquanpeng/one-api)
- [new-api](https://github.com/QuantumNous/new-api) 
- [Veloera](https://github.com/Veloera/Veloera)
- [one-hub](https://github.com/MartialBE/one-hub)
- [done-hub](https://github.com/deanxv/done-hub)



## ✨ 功能特性

- 🔍 **自动识别中转站点** - 自动创建系统访问 token 并添加到插件的站点列表中
- 💰 **自动识别中转站充值比例** - 智能解析站点配置信息
- 👥 **多账号管理** - 每个站点可添加多个账号
- 📊 **余额与日志查看** - 账号的余额、使用日志一目了然
- 🔑 **令牌(key)管理** - 便捷的密钥查看与管理
- 🤖 **模型信息查看** - 站点支持模型信息和渠道查看
- 🔒 **完全离线** - 插件无需联网,保护隐私安全

## 🖥️ 截图展示

![软件截图](./docs/docs/static/image/app_show.png)

## 🚀 安装使用

### 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)

<!-- ### 手动安装
1. 下载最新版本的扩展包
2. 打开 Chrome 浏览器,进入 `chrome://extensions/`
3. 开启 "开发者模式"
4. 点击 "加载已解压的扩展程序"
5. 选择解压后的扩展文件夹 -->

## 🛠️ 开发指南

### 环境要求
- Node.js 18+
- npm 或 pnpm

### 本地开发

```bash
# 克隆项目
git clone https://github.com/username/one-api-hub.git
cd one-api-hub

# 安装依赖
pnpm install
# 或者
npm install

# 启动开发服务器
pnpm dev
# 或者
npm run dev
```

然后在浏览器中加载 `build/chrome-mv3-dev` 目录作为扩展程序。

### 构建生产版本

```bash
pnpm build
# 或者 
npm run build
```

这将在 `build` 目录中创建生产版本的扩展包。

## 🔮 未来支持

- 🧪 **模型降智测试** - 自动化模型性能测试
- ☁️ **WebDAV 数据备份** - 云端数据同步与备份


## 👥 贡献者(不分先后)

感谢以下贡献者对项目的支持:

- [@qixing-jk](https://github.com/qixing-jk)
- [@JianKang-Li](https://github.com/JianKang-Li)


## 🏗️ 技术栈

- **框架**: [Plasmo](https://plasmo.com) v0.90.5
- **UI 库**: [React](https://reactjs.org) 18.2.0
- **样式**: [Tailwind CSS](https://tailwindcss.com) v3.4.17
- **组件**: [Headless UI](https://headlessui.com)
- **图标**: [Heroicons](https://heroicons.com)
- **状态管理**: [Zustand](https://zustand-demo.pmnd.rs)
- **类型检查**: [TypeScript](https://typescriptlang.org) 5.3.3


## 📄 许可证

本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。


## 🙏 致谢
- [Plasmo](https://plasmo.com) - 现代化的浏览器扩展开发框架

---

<div align="center">
  <strong>⭐ 如果这个项目对你有帮助,请考虑给它一个星标!</strong>
</div>

================================================
FILE: background.ts
================================================
import {
  autoRefreshService,
  handleAutoRefreshMessage
} from "./services/autoRefreshService"

// 管理临时窗口的 Map
const tempWindows = new Map<string, number>()

// 插件启动时初始化自动刷新服务
chrome.runtime.onStartup.addListener(async () => {
  console.log("[Background] 插件启动,初始化自动刷新服务")
  await autoRefreshService.initialize()
})

// 插件安装时初始化自动刷新服务
chrome.runtime.onInstalled.addListener(async () => {
  console.log("[Background] 插件安装/更新,初始化自动刷新服务")
  await autoRefreshService.initialize()
})

// 处理来自 popup 的消息
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
  if (request.action === "openTempWindow") {
    handleOpenTempWindow(request, sendResponse)
    return true // 保持异步响应通道
  }

  if (request.action === "closeTempWindow") {
    handleCloseTempWindow(request, sendResponse)
    return true
  }

  if (request.action === "autoDetectSite") {
    handleAutoDetectSite(request, sendResponse)
    return true
  }

  // 处理自动刷新相关消息
  if (
    (request.action && request.action.startsWith("autoRefresh")) ||
    [
      "setupAutoRefresh",
      "refreshNow",
      "stopAutoRefresh",
      "updateAutoRefreshSettings",
      "getAutoRefreshStatus"
    ].includes(request.action)
  ) {
    handleAutoRefreshMessage(request, sendResponse)
    return true
  }
})

// 打开临时窗口访问指定站点
async function handleOpenTempWindow(request: any, sendResponse: Function) {
  try {
    const { url, requestId } = request

    // 创建新窗口
    const window = await chrome.windows.create({
      url: url,
      type: "popup",
      width: 800,
      height: 600,
      focused: false
    })

    if (window.id) {
      // 记录窗口ID
      tempWindows.set(requestId, window.id)
      sendResponse({ success: true, windowId: window.id })
    } else {
      sendResponse({ success: false, error: "无法创建窗口" })
    }
  } catch (error) {
    sendResponse({ success: false, error: error.message })
  }
}

// 关闭临时窗口
async function handleCloseTempWindow(request: any, sendResponse: Function) {
  try {
    const { requestId } = request
    const windowId = tempWindows.get(requestId)

    if (windowId) {
      await chrome.windows.remove(windowId)
      tempWindows.delete(requestId)
    }

    sendResponse({ success: true })
  } catch (error) {
    sendResponse({ success: false, error: error.message })
  }
}

// 自动检测站点信息
async function handleAutoDetectSite(request: any, sendResponse: Function) {
  const { url, requestId } = request

  try {
    // 1. 打开临时窗口
    const window = await chrome.windows.create({
      url: url,
      type: "popup",
      width: 800,
      height: 600,
      focused: false
    })

    if (!window.id || !window.tabs?.[0]?.id) {
      throw new Error("无法创建窗口或获取标签页")
    }

    const windowId = window.id
    const tabId = window.tabs[0].id

    // 记录窗口
    tempWindows.set(requestId, windowId)

    // 2. 等待页面加载完成
    await waitForTabComplete(tabId)

    // 3. 通过 content script 获取用户信息
    const userResponse = await chrome.tabs.sendMessage(tabId, {
      action: "getUserFromLocalStorage",
      url: url
    })
    console.log(userResponse.error)

    if (!userResponse.success) {
      throw new Error(userResponse.error)
    }

    // 4. 关闭临时窗口
    await chrome.windows.remove(windowId)
    tempWindows.delete(requestId)

    // 5. 返回结果
    sendResponse({
      success: true,
      data: {
        userId: userResponse.data.userId,
        user: userResponse.data.user
      }
    })
  } catch (error) {
    // 清理窗口
    const windowId = tempWindows.get(requestId)
    if (windowId) {
      try {
        await chrome.windows.remove(windowId)
        tempWindows.delete(requestId)
      } catch (cleanupError) {
        console.log("清理窗口失败:", cleanupError)
      }
    }

    sendResponse({ success: false, error: error.message })
  }
}

// 等待标签页加载完成
function waitForTabComplete(tabId: number): Promise<void> {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error("页面加载超时"))
    }, 10000) // 10秒超时

    const checkStatus = () => {
      chrome.tabs.get(tabId, (tab) => {
        if (chrome.runtime.lastError) {
          clearTimeout(timeout)
          reject(new Error(chrome.runtime.lastError.message))
          return
        }

        if (tab.status === "complete") {
          clearTimeout(timeout)
          // 再等待一秒确保页面完全加载
          setTimeout(resolve, 1000)
        } else {
          setTimeout(checkStatus, 100)
        }
      })
    }

    checkStatus()
  })
}

// 监听窗口关闭事件,清理记录
chrome.windows.onRemoved.addListener((windowId) => {
  for (const [requestId, storedWindowId] of tempWindows.entries()) {
    if (storedWindowId === windowId) {
      tempWindows.delete(requestId)
      break
    }
  }
})


================================================
FILE: components/AccountList.tsx
================================================
import { ChevronUpIcon, ChevronDownIcon, ChartBarIcon, CpuChipIcon, EllipsisHorizontalIcon, DocumentDuplicateIcon, ChartPieIcon, PencilIcon, TrashIcon, ArrowPathIcon, InboxIcon, KeyIcon } from "@heroicons/react/24/outline"
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'
import CountUp from "react-countup"
import { UI_CONSTANTS, HEALTH_STATUS_MAP } from "../constants/ui"
import { getCurrencySymbol } from "../utils/formatters"
import type { DisplaySiteData } from "../types"
import { useState, useCallback, useRef, useEffect } from 'react'
import Tooltip from './Tooltip'
import DelAccountDialog from './DelAccountDialog'
import CopyKeyDialog from './CopyKeyDialog'

type SortField = 'name' | 'balance' | 'consumption'
type SortOrder = 'asc' | 'desc'

interface AccountListProps {
  // 数据
  sites: DisplaySiteData[]
  currencyType: 'USD' | 'CNY'
  
  // 排序状态
  sortField: SortField
  sortOrder: SortOrder
  
  // 动画相关
  isInitialLoad: boolean
  prevBalances: { [id: string]: { USD: number, CNY: number } }
  
  // 刷新状态
  refreshingAccountId?: string | null
  
  // 事件处理
  onSort: (field: SortField) => void
  onAddAccount: () => void
  onRefreshAccount?: (site: DisplaySiteData) => Promise<void>
  onCopyUrl?: (site: DisplaySiteData) => void
  onViewUsage?: (site: DisplaySiteData) => void
  onViewModels?: (site: DisplaySiteData) => void
  onEditAccount?: (site: DisplaySiteData) => void
  onDeleteAccount?: (site: DisplaySiteData) => void
  onViewKeys?: (site: DisplaySiteData) => void
}

export default function AccountList({
  sites,
  currencyType,
  sortField,
  sortOrder,
  isInitialLoad,
  prevBalances,
  refreshingAccountId,
  onSort,
  onAddAccount,
  onRefreshAccount,
  onCopyUrl,
  onViewUsage,
  onViewModels,
  onEditAccount,
  onDeleteAccount,
  onViewKeys
}: AccountListProps) {
  const [hoveredSiteId, setHoveredSiteId] = useState<string | null>(null)
  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null)
  const [deleteDialogAccount, setDeleteDialogAccount] = useState<DisplaySiteData | null>(null)
  const [copyKeyDialogAccount, setCopyKeyDialogAccount] = useState<DisplaySiteData | null>(null)

  // 防抖的 hover 处理
  const handleMouseEnter = useCallback((siteId: string) => {
    if (hoverTimeoutRef.current) {
      clearTimeout(hoverTimeoutRef.current)
    }
    hoverTimeoutRef.current = setTimeout(() => {
      setHoveredSiteId(siteId)
    }, 50) // 100ms 防抖延迟
  }, [])

  const handleMouseLeave = useCallback(() => {
    if (hoverTimeoutRef.current) {
      clearTimeout(hoverTimeoutRef.current)
    }
    hoverTimeoutRef.current = setTimeout(() => {
      setHoveredSiteId(null)
    }, 0) // 不需要离开时的延迟
  }, [])

  // 清理定时器
  useEffect(() => {
    return () => {
      if (hoverTimeoutRef.current) {
        clearTimeout(hoverTimeoutRef.current)
      }
    }
  }, [])

  const copyToClipboard = async (text: string) => {
    try {
      await navigator.clipboard.writeText(text)
    } catch (err) {
      console.error('Failed to copy:', err)
    }
  }

  const handleCopyUrl = (site: DisplaySiteData) => {
    copyToClipboard(site.baseUrl)
    onCopyUrl?.(site)
  }

  const handleCopyKey = (site: DisplaySiteData) => {
    setCopyKeyDialogAccount(site)
  }

  const handleRefreshAccount = async (site: DisplaySiteData) => {
    if (onRefreshAccount) {
      try {
        await onRefreshAccount(site)
      } catch (error) {
        console.error('刷新账号失败:', error)
      }
    }
  }
  if (sites.length === 0) {
    return (
      <div className="px-6 py-12 text-center">
        <InboxIcon className="w-16 h-16 text-gray-200 mx-auto mb-4" />
        <p className="text-gray-500 text-sm mb-4">暂无站点账号</p>
        <button 
          onClick={onAddAccount}
          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"
        >
          添加第一个站点账号
        </button>
      </div>
    )
  }

  const renderSortButton = (field: SortField, label: string) => (
    <button
      onClick={() => onSort(field)}
      className="flex items-center space-x-1 text-xs font-medium text-gray-500 hover:text-gray-700 transition-colors"
    >
      <span>{label}</span>
      {sortField === field && (
        sortOrder === 'asc' ? 
          <ChevronUpIcon className="w-3 h-3" /> : 
          <ChevronDownIcon className="w-3 h-3" />
      )}
    </button>
  )

  return (
    <div className="flex flex-col">
      {/* 表头 */}
      <div className="px-5 py-3 bg-gray-50 border-b border-gray-100 sticky top-0 z-10">
        <div className="flex items-center space-x-4">
          <div className="flex-1">
            {renderSortButton('name', '账号')}
          </div>
          <div className="text-right flex-shrink-0">
            <div className="flex items-center space-x-1">
              {renderSortButton('balance', '余额')}
              <span className="text-xs text-gray-400">/</span>
              {renderSortButton('consumption', '今日消耗')}
            </div>
          </div>
        </div>
      </div>
      
      {/* 账号列表 */}
      {sites.map((site) => (
        <div 
          key={site.id} 
          className="px-5 py-4 border-b border-gray-50 hover:bg-gray-25 transition-colors relative group"
          onMouseEnter={() => handleMouseEnter(site.id)}
          onMouseLeave={handleMouseLeave}
        >
          <div className="flex items-center space-x-4">
            {/* 站点信息 */}
            <div className="flex items-center space-x-3 flex-1 min-w-0">
              <div className="flex-1 min-w-0">
                <div className="flex items-center space-x-2 mb-0.5">
                  {/* 站点状态指示器 */}
                  <div className={`w-2 h-2 rounded-full flex-shrink-0 ${
                    HEALTH_STATUS_MAP[site.healthStatus]?.color || UI_CONSTANTS.STYLES.STATUS_INDICATOR.UNKNOWN
                  }`}></div>
                  <div className="font-medium text-gray-900 text-sm truncate">
                    <a
                      href={site.baseUrl}
                      target="_blank"
                      rel="noopener noreferrer"
                    >
                      {site.name}
                    </a>
                  </div>
                </div>
                <div className="text-xs text-gray-500 truncate ml-4">{site.username}</div>
              </div>
            </div>
            
            {/* 按钮组 - 只在 hover 时显示 */}
            {hoveredSiteId === site.id && (
              <div className="flex items-center space-x-2 flex-shrink-0">
                {/* 刷新按钮 */}
                <Tooltip content="刷新账号" position="top">
                  <button
                    onClick={() => handleRefreshAccount(site)}
                    className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 transition-colors"
                    disabled={refreshingAccountId === site.id}
                  >
                    <ArrowPathIcon 
                      className={`w-4 h-4 text-gray-500 ${
                        refreshingAccountId === site.id ? 'animate-spin' : ''
                      }`} 
                    />
                  </button>
                </Tooltip>

                {/* 复制下拉菜单 */}
                <Menu as="div" className="relative">
                  <Tooltip content="复制" position="top">
                    <MenuButton className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 transition-colors">
                      <DocumentDuplicateIcon className="w-4 h-4 text-gray-500" />
                    </MenuButton>
                  </Tooltip>
                  <MenuItems 
                    anchor="bottom end"
                    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]"
                  >
                    <MenuItem>
                      <button
                        onClick={() => handleCopyUrl(site)}
                        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"
                      >
                        <DocumentDuplicateIcon className="w-4 h-4" />
                        <span>复制 URL</span>
                      </button>
                    </MenuItem>
                    <MenuItem>
                      <button
                        onClick={() => handleCopyKey(site)}
                        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"
                      >
                        <DocumentDuplicateIcon className="w-4 h-4" />
                        <span>复制密钥</span>
                      </button>
                    </MenuItem>
                    <hr />
                    <MenuItem>
                      <button
                        onClick={() => onViewKeys?.(site)}
                        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"
                      >
                        <KeyIcon className="w-4 h-4" />
                        <span>管理密钥</span>
                      </button>
                    </MenuItem>
                  </MenuItems>
                </Menu>

                {/* 更多下拉菜单 */}
                <Menu as="div" className="relative">
                    <MenuButton className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 transition-colors">
                      <EllipsisHorizontalIcon className="w-4 h-4 text-gray-500" />
                    </MenuButton>
                  <MenuItems 
                    anchor="bottom end"
                    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]"
                  >
                    <MenuItem>
                      <button
                        onClick={() => onViewModels?.(site)}
                        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"
                      >
                        <CpuChipIcon className="w-4 h-4" />
                        <span>模型</span>
                      </button>
                    </MenuItem>
                    <MenuItem>
                      <button
                        onClick={() => onViewUsage?.(site)}
                        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"
                      >
                        <ChartPieIcon className="w-4 h-4" />
                        <span>用量</span>
                      </button>
                    </MenuItem>
                    <hr />
                    <MenuItem>
                      <button
                        onClick={() => onEditAccount?.(site)}
                        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"
                      >
                        <PencilIcon className="w-4 h-4" />
                        <span>编辑</span>
                      </button>
                    </MenuItem>
                    <MenuItem>
                      <button
                        onClick={() => setDeleteDialogAccount(site)}
                        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"
                      >
                        <TrashIcon className="w-4 h-4" />
                        <span>删除</span>
                      </button>
                    </MenuItem>
                  </MenuItems>
                </Menu>
              </div>
            )}
            
            {/* 余额和统计 */}
            <div className="text-right flex-shrink-0">
              <div className="font-semibold text-gray-900 text-lg mb-0.5">
                {getCurrencySymbol(currencyType)}
                <CountUp
                  start={isInitialLoad ? 0 : (prevBalances[site.id]?.[currencyType] || 0)}
                  end={site.balance[currencyType]}
                  duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.SLOW_DURATION : UI_CONSTANTS.ANIMATION.FAST_DURATION}
                  decimals={2}
                  preserveValue
                />
              </div>
              <div className={`text-xs ${site.todayConsumption[currencyType] > 0 ? 'text-green-500' : 'text-gray-400'}`}>
                -{getCurrencySymbol(currencyType)}
                <CountUp
                  start={isInitialLoad ? 0 : 0}
                  end={site.todayConsumption[currencyType]}
                  duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.SLOW_DURATION : UI_CONSTANTS.ANIMATION.FAST_DURATION}
                  decimals={2}
                  preserveValue
                />
              </div>
            </div>
          </div>
        </div>
      ))}
      
      {/* 删除账号确认对话框 */}
      <DelAccountDialog
        isOpen={deleteDialogAccount !== null}
        onClose={() => setDeleteDialogAccount(null)}
        account={deleteDialogAccount}
        onDeleted={() => onDeleteAccount?.(deleteDialogAccount!)}
      />

      {/* 复制密钥对话框 */}
      <CopyKeyDialog
        isOpen={copyKeyDialogAccount !== null}
        onClose={() => setCopyKeyDialogAccount(null)}
        account={copyKeyDialogAccount}
      />
    </div>
  )
}

================================================
FILE: components/ActionButtons.tsx
================================================
import { PlusIcon, KeyIcon, CpuChipIcon } from "@heroicons/react/24/outline"
import { UI_CONSTANTS } from "../constants/ui"
import Tooltip from "./Tooltip"

interface ActionButtonsProps {
  onAddAccount: () => void
  onViewKeys: () => void
  onViewModels: () => void
}

export default function ActionButtons({ onAddAccount, onViewKeys, onViewModels }: ActionButtonsProps) {
  return (
    <div className="px-5 py-4 bg-gray-50/50">
      <div className="flex space-x-2">
        <button 
          onClick={onAddAccount}
          className={UI_CONSTANTS.STYLES.BUTTON.PRIMARY}
        >
          <PlusIcon className="w-4 h-4" />
          <span>新增账号</span>
        </button>
        <Tooltip content="密钥管理">
          <button 
            onClick={onViewKeys}
            className={UI_CONSTANTS.STYLES.BUTTON.SECONDARY}
          >
            <KeyIcon className="w-4 h-4" />
          </button>
        </Tooltip>
        <Tooltip content="模型列表">
          <button 
            onClick={onViewModels}
            className={UI_CONSTANTS.STYLES.BUTTON.SECONDARY}
          >
            <CpuChipIcon className="w-4 h-4" />
          </button>
        </Tooltip>
      </div>
    </div>
  )
}

================================================
FILE: components/AddAccountDialog.tsx
================================================
import { useState, useEffect, Fragment } from "react"
import toast from 'react-hot-toast'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react"
import { GlobeAltIcon, XMarkIcon, SparklesIcon, UserIcon, KeyIcon, EyeIcon, EyeSlashIcon, CurrencyDollarIcon } from "@heroicons/react/24/outline"
import { autoDetectAccount, validateAndSaveAccount, extractDomainPrefix, isValidExchangeRate } from "../services/accountOperations"
import AutoDetectErrorAlert from "./AutoDetectErrorAlert"
import type { AutoDetectError } from "../utils/autoDetectUtils"

interface AddAccountDialogProps {
  isOpen: boolean
  onClose: () => void
}

export default function AddAccountDialog({ isOpen, onClose }: AddAccountDialogProps) {
  const [url, setUrl] = useState("")
  const [isDetecting, setIsDetecting] = useState(false)
  const [siteName, setSiteName] = useState("")
  const [username, setUsername] = useState("")
  const [accessToken, setAccessToken] = useState("")
  const [userId, setUserId] = useState("")
  const [isDetected, setIsDetected] = useState(false)
  const [isSaving, setIsSaving] = useState(false)
  const [showAccessToken, setShowAccessToken] = useState(false)
  const [detectionError, setDetectionError] = useState<AutoDetectError | null>(null)
  const [showManualForm, setShowManualForm] = useState(false)
  const [exchangeRate, setExchangeRate] = useState("")
  const [currentTabUrl, setCurrentTabUrl] = useState<string | null>(null)


  useEffect(() => {
    if (isOpen) {
      // 重置状态
      setIsDetected(false)
      setSiteName("")
      setUsername("")
      setAccessToken("")
      setUserId("")
      setShowAccessToken(false)
      setDetectionError(null)
      setShowManualForm(false)
      setExchangeRate("")
      setCurrentTabUrl(null)
      setUrl("")
      
      // 获取当前标签页的 URL 作为初始参考
      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        if (tabs[0]?.url) {
          try {
            const urlObj = new URL(tabs[0].url)
            const baseUrl = `${urlObj.protocol}//${urlObj.host}`
            // 如果站点不是以http开头,则不处理(可能为空白页)
            if (!baseUrl.startsWith('http')) {
              return
            }
            setCurrentTabUrl(baseUrl)
            // 设置站点名称为域名前缀,但不自动填入URL
            const domainPrefix = extractDomainPrefix(urlObj.hostname)
            setSiteName(domainPrefix)
          } catch (error) {
            console.log('无法解析 URL:', error)
            setCurrentTabUrl(null)
            setSiteName("")
          }
        }
      })
    }
  }, [isOpen])

  // 处理点击当前标签页 URL
  const handleUseCurrentTabUrl = () => {
    if (currentTabUrl) {
      setUrl(currentTabUrl)
    }
  }

  const handleAutoDetect = async () => {
    if (!url.trim()) {
      return
    }

    setIsDetecting(true)
    setDetectionError(null)
    
    try {
      const result = await autoDetectAccount(url.trim())
      
      if (!result.success) {
        setDetectionError(result.detailedError || null)
        setShowManualForm(true)
        return
      }

      if (result.data) {
        // 更新表单数据
        setUsername(result.data.username)
        setAccessToken(result.data.accessToken)
        setUserId(result.data.userId)
        
        // 设置充值比例默认值
        if (result.data.exchangeRate) {
          setExchangeRate(result.data.exchangeRate.toString())
          console.log('获取到默认充值比例:', result.data.exchangeRate)
        } else {
          setExchangeRate("") // 如果没有获取到,设置为空
          console.log('未获取到默认充值比例,设置为空')
        }
        
        setIsDetected(true)
        
        console.log('自动识别成功:', { 
          username: result.data.username, 
          siteName, 
          exchangeRate: result.data.exchangeRate 
        })
      }
    } catch (error) {
      console.error('自动识别失败:', error)
      const errorMessage = error instanceof Error ? error.message : '未知错误'
      // 使用通用错误处理
      setDetectionError({
        type: 'unknown' as any,
        message: `自动识别失败: ${errorMessage}`,
        helpDocUrl: '#'
      })
      setShowManualForm(true) // 识别失败后显示手动表单
    } finally {
      setIsDetecting(false)
    }
  }


  const handleSaveAccount = async () => {
    setIsSaving(true)
    
    try {
      await toast.promise(
        validateAndSaveAccount(
          url.trim(),
          siteName.trim(),
          username.trim(),
          accessToken.trim(),
          userId.trim(),
          exchangeRate
        ),
        {
          loading: '正在添加账号...',
          success: (result) => {
            if (result.success) {
              onClose()
              return `账号 ${siteName} 添加成功!`
            } else {
              throw new Error(result.error || '保存失败')
            }
          },
          error: (err) => {
            const errorMsg = err.message || '添加失败'
            return `添加失败: ${errorMsg}`
          },
        }
      )
    } catch (error) {
      console.error('保存账号失败:', error)
    } finally {
      setIsSaving(false)
    }
  }

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (isDetected || showManualForm) {
      handleSaveAccount()
    } else {
      handleAutoDetect()
    }
  }

  return (
    <Transition show={isOpen} as={Fragment}>
      <Dialog
        onClose={onClose}
        className="relative z-50"
      >
        {/* 背景遮罩动画 */}
        <TransitionChild
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black/30 backdrop-blur-sm" aria-hidden="true" />
        </TransitionChild>
        
        {/* 居中容器 - 针对插件优化 */}
        <div className="fixed inset-0 flex items-center justify-center p-2">
          {/* 弹窗面板动画 */}
          <TransitionChild
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0 scale-95 translate-y-4"
            enterTo="opacity-100 scale-100 translate-y-0"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 scale-100 translate-y-0"
            leaveTo="opacity-0 scale-95 translate-y-4"
          >
            <DialogPanel className="w-full max-w-sm bg-white rounded-lg shadow-xl transform transition-all max-h-[90vh] overflow-y-auto">
              {/* 头部 */}
              <div className="flex items-center justify-between p-4 border-b border-gray-100">
                <div className="flex items-center space-x-3">
                  <div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
                    <SparklesIcon className="w-4 h-4 text-white" />
                  </div>
                  <DialogTitle className="text-lg font-semibold text-gray-900">
                    新增账号
                  </DialogTitle>
                </div>
                <button
                  onClick={onClose}
                  className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
                >
                  <XMarkIcon className="w-5 h-5" />
                </button>
              </div>

              {/* 内容区域 */}
              <div className="p-4">
                <form onSubmit={handleSubmit} className="space-y-6">
                  {/* 识别错误提示 */}
                  {detectionError && (
                    <AutoDetectErrorAlert 
                      error={detectionError}
                      siteUrl={url}
                    />
                  )}

                  {/* URL 输入框 */}
                  <div>
                    <label className="block text-sm font-medium text-gray-700 mb-2">
                      站点地址
                    </label>
                    <div className="relative">
                      <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                        <GlobeAltIcon className="h-5 w-5 text-gray-400" />
                      </div>
                      <input
                        type="url"
                        value={url}
                        onChange={(e) => {
                          const inputUrl = e.target.value
                          
                          // 当用户输入 URL 时,提取协议和主机部分
                          if (inputUrl.trim()) {
                            try {
                              const urlObj = new URL(inputUrl)
                              // 只保留协议和主机部分,不带路径
                              const baseUrl = `${urlObj.protocol}//${urlObj.host}`
                              setUrl(baseUrl)
                              
                              // 自动更新站点名称
                              const domainPrefix = extractDomainPrefix(urlObj.hostname)
                              setSiteName(domainPrefix)
                            } catch (error) {
                              // 如果 URL 格式不完整,先保存用户输入,但尝试提取域名
                              setUrl(inputUrl)
                              const match = inputUrl.match(/\/\/([^\/]+)/)
                              if (match) {
                                const domainPrefix = extractDomainPrefix(match[1])
                                setSiteName(domainPrefix)
                              }
                            }
                          } else {
                            setUrl("")
                            setSiteName("")
                          }
                        }}
                        placeholder="https://example.com"
                        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"
                        required
                        disabled={isDetected}
                      />
                      {url && (
                        <button
                          type="button"
                          onClick={() => setUrl('')}
                          className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
                          disabled={isDetected}
                        >
                          <XMarkIcon className="h-4 w-4" />
                        </button>
                      )}
                    </div>
                    <p className="mt-2 text-xs text-gray-500">
                      请输入 One API 或 New API 站点的完整地址
                    </p>
                    {/* 当前标签页 URL 提示 */}
                    <Transition
                      show={!!(currentTabUrl && !url)}
                      as={Fragment}
                    >
                      <TransitionChild
                        as="div"
                        enter="ease-out duration-500 delay-500"
                        enterFrom="opacity-0 translate-y-3 scale-90"
                        enterTo="opacity-100 translate-y-0 scale-100"
                        leave="ease-in duration-200"
                        leaveFrom="opacity-100 translate-y-0 scale-100"
                        leaveTo="opacity-0 translate-y-2 scale-95"
                        className="mt-2"
                      >
                        <button
                          type="button"
                          onClick={handleUseCurrentTabUrl}
                          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"
                        >
                          <GlobeAltIcon className="w-3 h-3 mr-1.5 animate-pulse" />
                          <span>使用当前: {currentTabUrl && new URL(currentTabUrl).host}</span>
                        </button>
                      </TransitionChild>
                    </Transition>
                  </div>


                  {/* 识别成功后的表单或手动添加表单 */}
                  {(isDetected || showManualForm) && (
                    <>
                      {/* 网站名称 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-2">
                          网站名称
                        </label>
                        <div className="relative">
                          <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                            <GlobeAltIcon className="h-5 w-5 text-gray-400" />
                          </div>
                          <input
                            type="text"
                            value={siteName}
                            onChange={(e) => setSiteName(e.target.value)}
                            placeholder="example.com"
                            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"
                            required
                          />
                        </div>
                      </div>

                      {/* 用户名 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-2">
                          用户名
                        </label>
                        <div className="relative">
                          <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                            <UserIcon className="h-5 w-5 text-gray-400" />
                          </div>
                          <input
                            type="text"
                            value={username}
                            onChange={(e) => setUsername(e.target.value)}
                            placeholder="用户名"
                            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"
                            required
                          />
                        </div>
                      </div>

                      {/* 用户 ID */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-2">
                          用户 ID
                        </label>
                        <div className="relative">
                          <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                            <span className="text-gray-400 font-mono text-sm">#</span>
                          </div>
                          <input
                            type="number"
                            value={userId}
                            onChange={(e) => setUserId(e.target.value)}
                            placeholder="用户 ID (数字)"
                            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"
                            required
                          />
                        </div>
                      </div>

                      {/* 访问令牌 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-2">
                          访问令牌
                        </label>
                        <div className="relative">
                          <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                            <KeyIcon className="h-5 w-5 text-gray-400" />
                          </div>
                          <input
                            type={showAccessToken ? "text" : "password"}
                            value={accessToken}
                            onChange={(e) => setAccessToken(e.target.value)}
                            placeholder="访问令牌"
                            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"
                            required
                          />
                          <button
                            type="button"
                            onClick={() => setShowAccessToken(!showAccessToken)}
                            className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
                          >
                            {showAccessToken ? (
                              <EyeSlashIcon className="h-4 w-4" />
                            ) : (
                              <EyeIcon className="h-4 w-4" />
                            )}
                          </button>
                        </div>
                      </div>

                      {/* 充值金额比例 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-2">
                          充值金额比例 (CNY/USD)
                        </label>
                        <div className="relative">
                          <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                            <CurrencyDollarIcon className="h-5 w-5 text-gray-400" />
                          </div>
                          <input
                            type="number"
                            step="0.1"
                            min="0.1"
                            max="100"
                            value={exchangeRate}
                            onChange={(e) => setExchangeRate(e.target.value)}
                            placeholder="请输入充值比例"
                            className={`block w-full pl-10 py-3 border rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 transition-colors ${
                              isValidExchangeRate(exchangeRate) 
                                ? 'border-gray-200 focus:ring-blue-500 focus:border-transparent' 
                                : 'border-red-300 focus:ring-red-500 focus:border-red-500'
                            }`}
                            required
                          />
                          <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
                            <span className="text-sm text-gray-500">CNY</span>
                          </div>
                        </div>
                        <p className="mt-1 text-xs text-gray-500">
                          表示充值 1 美元需要多少人民币。系统会尝试自动获取,如未获取到请手动填写
                        </p>
                        {!isValidExchangeRate(exchangeRate) && exchangeRate && (
                          <p className="mt-1 text-xs text-red-600">
                            请输入有效的汇率 (0.1 - 100)
                          </p>
                        )}
                      </div>
                    </>
                  )}

                  {/* 按钮组 */}
                  <div className="flex space-x-3 pt-2">
                    <button
                      type="button"
                      onClick={onClose}
                      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"
                    >
                      取消
                    </button>
                    {isDetected ? (
                      <button
                        type="submit"
                        disabled={!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim() || !isValidExchangeRate(exchangeRate) || isSaving}
                        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"
                      >
                        {isSaving ? (
                          <>
                            <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
                            <span>保存中...</span>
                          </>
                        ) : (
                          <>
                            <SparklesIcon className="w-4 h-4" />
                            <span>确认添加</span>
                          </>
                        )}
                      </button>
                    ) : showManualForm ? (
                      <button
                        type="submit"
                        disabled={!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim() || !isValidExchangeRate(exchangeRate) || isSaving}
                        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"
                      >
                        {isSaving ? (
                          <>
                            <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
                            <span>保存中...</span>
                          </>
                        ) : (
                          <>
                            <SparklesIcon className="w-4 h-4" />
                            <span>手动添加</span>
                          </>
                        )}
                      </button>
                    ) : (
                      <button
                        type="submit"
                        disabled={!url.trim() || isDetecting}
                        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"
                      >
                        {isDetecting ? (
                          <>
                            <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
                            <span>识别中...</span>
                          </>
                        ) : (
                          <>
                            <SparklesIcon className="w-4 h-4" />
                            <span>自动识别</span>
                          </>
                        )}
                      </button>
                    )}
                  </div>
                  
                  {/* 手动添加按钮 - 在自动识别失败后显示 */}
                  {!isDetected && !showManualForm && detectionError && (
                    <div className="pt-2">
                      <button
                        type="button"
                        onClick={() => setShowManualForm(true)}
                        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"
                      >
                        <UserIcon className="w-4 h-4" />
                        <span>手动添加账号信息</span>
                      </button>
                    </div>
                  )}
                </form>
              </div>

              {/* 提示信息 */}
              <div className="px-4 pb-4">
                <div className="bg-blue-50 border border-blue-100 rounded-lg p-3">
                  <div className="flex">
                    <div className="flex-shrink-0">
                      <SparklesIcon className="h-5 w-5 text-blue-400" />
                    </div>
                    <div className="ml-3">
                      <h3 className="text-xs font-medium text-blue-800">
                        {isDetected ? '账号信息确认' : showManualForm ? '手动添加' : '自动识别'}
                      </h3>
                      <div className="mt-1 text-xs text-blue-700">
                        <p>
                          {isDetected 
                            ? '请确认账号信息无误后点击"确认添加"按钮。'
                            : showManualForm
                            ? '请手动填写账号信息。账号将被安全地保存在本地存储中。'
                            : '请先在目标站点进行登录,插件将自动检测站点类型,并自动获取访问令牌。'
                          }
                        </p>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </Dialog>
    </Transition>
  )
}

================================================
FILE: components/AddTokenDialog.tsx
================================================
import { useState, useEffect, Fragment } from 'react'
import { Dialog, Transition, Switch } from '@headlessui/react'
import { 
  XMarkIcon, 
  KeyIcon,
  ExclamationTriangleIcon
} from '@heroicons/react/24/outline'
import { 
  fetchAvailableModels, 
  fetchUserGroups, 
  createApiToken,
  fetchTokenById,
  updateApiToken,
  type GroupInfo,
  type CreateTokenRequest,
  type ApiToken
} from '../services/apiService'
import { UI_CONSTANTS } from '../constants/ui'
import toast from 'react-hot-toast'

interface AddTokenDialogProps {
  isOpen: boolean
  onClose: () => void
  availableAccounts: Array<{
    id: string
    name: string
    baseUrl: string
    userId: number
    token: string
  }>
  preSelectedAccountId?: string | null
  editingToken?: ApiToken & { accountName: string } | null
}

interface FormData {
  accountId: string
  name: string
  quota: string
  expiredTime: string
  unlimitedQuota: boolean
  modelLimitsEnabled: boolean
  modelLimits: string[]
  allowIps: string
  group: string
}

export default function AddTokenDialog({ isOpen, onClose, availableAccounts, preSelectedAccountId, editingToken }: AddTokenDialogProps) {
  const [formData, setFormData] = useState<FormData>({
    accountId: '',
    name: '',
    quota: '',
    expiredTime: '',
    unlimitedQuota: true,
    modelLimitsEnabled: false,
    modelLimits: [],
    allowIps: '',
    group: 'default'
  })

  const [isLoading, setIsLoading] = useState(false)
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [availableModels, setAvailableModels] = useState<string[]>([])
  const [groups, setGroups] = useState<Record<string, GroupInfo>>({})
  const [errors, setErrors] = useState<Record<string, string>>({})

  // 获取当前选中的账号
  const currentAccount = availableAccounts.find(acc => acc.id === formData.accountId)
  
  // 判断是否为编辑模式
  const isEditMode = !!editingToken

  // 初始化表单数据
  useEffect(() => {
    if (isOpen) {
      if (isEditMode && editingToken) {
        // 编辑模式:从 editingToken 填充表单数据
        const matchingAccount = availableAccounts.find(acc => acc.name === editingToken.accountName)
        const accountId = matchingAccount?.id || (availableAccounts.length > 0 ? availableAccounts[0].id : '')
        
        setFormData({
          accountId,
          name: editingToken.name,
          quota: editingToken.unlimited_quota ? '' : (editingToken.remain_quota / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR).toString(),
          expiredTime: editingToken.expired_time === -1 ? '' : new Date(editingToken.expired_time * 1000).toISOString().slice(0, 16),
          unlimitedQuota: editingToken.unlimited_quota,
          modelLimitsEnabled: editingToken.model_limits_enabled || false,
          modelLimits: editingToken.model_limits ? editingToken.model_limits.split(',') : [],
          allowIps: editingToken.allow_ips || '',
          group: editingToken.group || 'default'
        })
      } else {
        // 创建模式:使用默认值
        const defaultAccountId = preSelectedAccountId || (availableAccounts.length > 0 ? availableAccounts[0].id : '')
        setFormData({
          accountId: defaultAccountId,
          name: '',
          quota: '',
          expiredTime: '',
          unlimitedQuota: true,
          modelLimitsEnabled: false,
          modelLimits: [],
          allowIps: '',
          group: 'default'
        })
      }
    }
  }, [isOpen, preSelectedAccountId, availableAccounts, isEditMode, editingToken])

  // 加载数据
  useEffect(() => {
    if (isOpen && currentAccount) {
      loadInitialData()
    }
  }, [isOpen, currentAccount])

  const loadInitialData = async () => {
    if (!currentAccount) return
    
    setIsLoading(true)
    try {
      const [models, groupsData] = await Promise.all([
        fetchAvailableModels(currentAccount.baseUrl, currentAccount.userId, currentAccount.token),
        fetchUserGroups(currentAccount.baseUrl, currentAccount.userId, currentAccount.token)
      ])
      
      setAvailableModels(models)
      setGroups(groupsData)
      
      // 设置默认分组
      if (groupsData.default) {
        setFormData(prev => ({ ...prev, group: 'default' }))
      } else {
        const firstGroup = Object.keys(groupsData)[0]
        if (firstGroup) {
          setFormData(prev => ({ ...prev, group: firstGroup }))
        }
      }
    } catch (error) {
      console.error('加载初始数据失败:', error)
      toast.error('加载数据失败,请稍后重试')
    } finally {
      setIsLoading(false)
    }
  }

  // 验证表单
  const validateForm = (): boolean => {
    const newErrors: Record<string, string> = {}

    if (!formData.accountId) {
      newErrors.accountId = '请选择账号'
    }

    if (!formData.name.trim()) {
      newErrors.name = '密钥名称不能为空'
    }

    if (!formData.unlimitedQuota) {
      const quota = parseFloat(formData.quota)
      if (isNaN(quota) || quota <= 0) {
        newErrors.quota = '请输入有效的额度金额'
      }
    }

    if (formData.expiredTime) {
      const expiredDate = new Date(formData.expiredTime)
      if (expiredDate <= new Date()) {
        newErrors.expiredTime = '过期时间必须大于当前时间'
      }
    }

    if (formData.allowIps && !isValidIpList(formData.allowIps)) {
      newErrors.allowIps = '请输入有效的IP地址,多个IP用逗号分隔'
    }

    setErrors(newErrors)
    return Object.keys(newErrors).length === 0
  }

  // 验证IP地址列表
  const isValidIpList = (ips: string): boolean => {
    const ipList = ips.split(',').map(ip => ip.trim())
    const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/
    
    return ipList.every(ip => {
      if (!ip) return false
      if (ip === '*') return true // 允许通配符
      return ipRegex.test(ip) && ip.split('.').every(part => {
        const num = parseInt(part)
        return num >= 0 && num <= 255
      })
    })
  }

  // 处理表单提交
  const handleSubmit = async () => {
    if (!currentAccount || !validateForm()) return

    setIsSubmitting(true)
    try {
      // 准备请求数据
      const tokenData: CreateTokenRequest = {
        name: formData.name.trim(),
        remain_quota: formData.unlimitedQuota ? -1 : Math.floor(parseFloat(formData.quota) * UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR),
        expired_time: formData.expiredTime ? Math.floor(new Date(formData.expiredTime).getTime() / 1000) : -1,
        unlimited_quota: formData.unlimitedQuota,
        model_limits_enabled: formData.modelLimitsEnabled,
        model_limits: formData.modelLimits.join(','),
        allow_ips: formData.allowIps.trim() || '',
        group: formData.group
      }

      if (isEditMode && editingToken) {
        // 编辑模式
        await updateApiToken(currentAccount.baseUrl, currentAccount.userId, currentAccount.token, editingToken.id, tokenData)
        toast.success('密钥更新成功')
      } else {
        // 创建模式
        await createApiToken(currentAccount.baseUrl, currentAccount.userId, currentAccount.token, tokenData)
        toast.success('密钥创建成功')
      }
      
      handleClose()
    } catch (error) {
      console.error(`${isEditMode ? '更新' : '创建'}密钥失败:`, error)
      toast.error(`${isEditMode ? '更新' : '创建'}密钥失败,请稍后重试`)
    } finally {
      setIsSubmitting(false)
    }
  }

  // 关闭对话框
  const handleClose = () => {
    setFormData({
      accountId: '',
      name: '',
      quota: '',
      expiredTime: '',
      unlimitedQuota: true,
      modelLimitsEnabled: false,
      modelLimits: [],
      allowIps: '',
      group: 'default'
    })
    setErrors({})
    setAvailableModels([])
    setGroups({})
    onClose()
  }

  return (
    <Transition appear show={isOpen} as={Fragment}>
      <Dialog as="div" className="relative z-50" onClose={handleClose}>
        <Transition.Child
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black bg-opacity-25" />
        </Transition.Child>

        <div className="fixed inset-0 overflow-y-auto">
          <div className="flex min-h-full items-center justify-center p-4">
            <Transition.Child
              as={Fragment}
              enter="ease-out duration-300"
              enterFrom="opacity-0 scale-95"
              enterTo="opacity-100 scale-100"
              leave="ease-in duration-200"
              leaveFrom="opacity-100 scale-100"
              leaveTo="opacity-0 scale-95"
            >
              <Dialog.Panel className="w-full max-w-2xl transform overflow-hidden rounded-lg bg-white p-6 shadow-xl transition-all">
                {/* 标题栏 */}
                <div className="flex items-center justify-between mb-6">
                  <div className="flex items-center space-x-2">
                    <KeyIcon className="w-6 h-6 text-blue-600" />
                    <Dialog.Title className="text-lg font-semibold text-gray-900">
                      {isEditMode ? '编辑API密钥' : '添加API密钥'}
                    </Dialog.Title>
                  </div>
                  <button
                    onClick={handleClose}
                    className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
                  >
                    <XMarkIcon className="w-5 h-5" />
                  </button>
                </div>

                {isLoading ? (
                  <div className="space-y-4">
                    <div className="animate-pulse">
                      <div className="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
                      <div className="space-y-3">
                        <div className="h-10 bg-gray-200 rounded"></div>
                        <div className="h-10 bg-gray-200 rounded"></div>
                        <div className="h-10 bg-gray-200 rounded"></div>
                      </div>
                    </div>
                  </div>
                ) : (
                  <div className="space-y-6">
                    {/* 基本信息 */}
                    <div className="space-y-4">
                      <h3 className="text-sm font-medium text-gray-900">基本信息</h3>
                      
                      {/* 账号选择 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          选择账号 <span className="text-red-500">*</span>
                        </label>
                        <select
                          value={formData.accountId}
                          onChange={(e) => setFormData(prev => ({ ...prev, accountId: e.target.value }))}
                          disabled={isEditMode} // 编辑模式下禁用账号选择
                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            errors.accountId ? 'border-red-300' : 'border-gray-300'
                          } ${isEditMode ? 'bg-gray-100 cursor-not-allowed' : ''}`}
                        >
                          <option value="">请选择账号</option>
                          {availableAccounts.map(account => (
                            <option key={account.id} value={account.id}>
                              {account.name}
                            </option>
                          ))}
                        </select>
                        {errors.accountId && (
                          <p className="mt-1 text-xs text-red-600">{errors.accountId}</p>
                        )}
                        {isEditMode && (
                          <p className="mt-1 text-xs text-gray-500">编辑模式下无法更改账号</p>
                        )}
                      </div>
                      
                      {/* 密钥名称 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          密钥名称 <span className="text-red-500">*</span>
                        </label>
                        <input
                          type="text"
                          value={formData.name}
                          onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            errors.name ? 'border-red-300' : 'border-gray-300'
                          }`}
                          placeholder="请输入密钥名称"
                        />
                        {errors.name && (
                          <p className="mt-1 text-xs text-red-600">{errors.name}</p>
                        )}
                      </div>

                      {/* 额度设置 */}
                      <div className="space-y-3">
                        <div className="flex items-center justify-between">
                          <label className="text-sm font-medium text-gray-700">额度设置</label>
                          <div className="flex items-center space-x-2">
                            <span className="text-sm text-gray-500">无限额度</span>
                            <Switch
                              checked={formData.unlimitedQuota}
                              onChange={(checked) => setFormData(prev => ({ ...prev, unlimitedQuota: checked }))}
                              className={`${
                                formData.unlimitedQuota ? 'bg-blue-600' : 'bg-gray-200'
                              } 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`}
                            >
                              <span
                                className={`${
                                  formData.unlimitedQuota ? 'translate-x-6' : 'translate-x-1'
                                } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
                              />
                            </Switch>
                          </div>
                        </div>
                        
                        {!formData.unlimitedQuota && (
                          <div>
                            <input
                              type="number"
                              step="0.01"
                              min="0"
                              value={formData.quota}
                              onChange={(e) => setFormData(prev => ({ ...prev, quota: e.target.value }))}
                              className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                                errors.quota ? 'border-red-300' : 'border-gray-300'
                              }`}
                              placeholder="请输入额度金额(美元)"
                            />
                            {errors.quota && (
                              <p className="mt-1 text-xs text-red-600">{errors.quota}</p>
                            )}
                            <p className="mt-1 text-xs text-gray-500">
                              1美元 = {UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR.toLocaleString()} 配额点数
                            </p>
                          </div>
                        )}
                      </div>

                      {/* 过期时间 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          过期时间
                        </label>
                        <input
                          type="datetime-local"
                          value={formData.expiredTime}
                          onChange={(e) => setFormData(prev => ({ ...prev, expiredTime: e.target.value }))}
                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            errors.expiredTime ? 'border-red-300' : 'border-gray-300'
                          }`}
                        />
                        {errors.expiredTime && (
                          <p className="mt-1 text-xs text-red-600">{errors.expiredTime}</p>
                        )}
                        <p className="mt-1 text-xs text-gray-500">留空表示永不过期</p>
                      </div>
                    </div>

                    {/* 高级设置 */}
                    <div className="space-y-4">
                      <h3 className="text-sm font-medium text-gray-900">高级设置</h3>
                      
                      {/* 分组选择 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          分组
                        </label>
                        <select
                          value={formData.group}
                          onChange={(e) => setFormData(prev => ({ ...prev, group: e.target.value }))}
                          className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                        >
                          {Object.entries(groups).map(([key, group]) => (
                            <option key={key} value={key}>
                              {group.desc} (倍率: {group.ratio})
                            </option>
                          ))}
                        </select>
                      </div>

                      {/* 模型限制 */}
                      <div className="space-y-3">
                        <div className="flex items-center justify-between">
                          <label className="text-sm font-medium text-gray-700">模型限制</label>
                          <Switch
                            checked={formData.modelLimitsEnabled}
                            onChange={(enabled) => setFormData(prev => ({ 
                              ...prev, 
                              modelLimitsEnabled: enabled,
                              modelLimits: enabled ? prev.modelLimits : []
                            }))}
                            className={`${
                              formData.modelLimitsEnabled ? 'bg-blue-600' : 'bg-gray-200'
                            } 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`}
                          >
                            <span
                              className={`${
                                formData.modelLimitsEnabled ? 'translate-x-6' : 'translate-x-1'
                              } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
                            />
                          </Switch>
                        </div>

                        {formData.modelLimitsEnabled && (
                          <div>
                            <select
                              multiple
                              value={formData.modelLimits}
                              onChange={(e) => {
                                const values = Array.from(e.target.selectedOptions, option => option.value)
                                setFormData(prev => ({ ...prev, modelLimits: values }))
                              }}
                              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"
                            >
                              {availableModels.map((model) => (
                                <option key={model} value={model}>
                                  {model}
                                </option>
                              ))}
                            </select>
                            <p className="mt-1 text-xs text-gray-500">
                              按住 Ctrl/Cmd 键可多选模型,已选择 {formData.modelLimits.length} 个模型
                            </p>
                          </div>
                        )}
                      </div>

                      {/* IP 限制 */}
                      <div>
                        <label className="block text-sm font-medium text-gray-700 mb-1">
                          IP限制
                        </label>
                        <input
                          type="text"
                          value={formData.allowIps}
                          onChange={(e) => setFormData(prev => ({ ...prev, allowIps: e.target.value }))}
                          className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            errors.allowIps ? 'border-red-300' : 'border-gray-300'
                          }`}
                          placeholder="留空表示不限制,多个IP用逗号分隔"
                        />
                        {errors.allowIps && (
                          <p className="mt-1 text-xs text-red-600">{errors.allowIps}</p>
                        )}
                        <p className="mt-1 text-xs text-gray-500">
                          例如: 192.168.1.1,10.0.0.1 或使用 * 表示不限制
                        </p>
                      </div>
                    </div>

                    {/* 警告提示 */}
                    <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
                      <div className="flex items-start space-x-2">
                        <ExclamationTriangleIcon className="w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0" />
                        <div className="text-sm text-yellow-800">
                          <p className="font-medium mb-1">注意事项</p>
                          <ul className="text-xs space-y-1">
                            <li>• 请妥善保管API密钥,避免泄露</li>
                          </ul>
                        </div>
                      </div>
                    </div>

                    {/* 操作按钮 */}
                    <div className="flex justify-end space-x-3 pt-4">
                      <button
                        onClick={handleClose}
                        disabled={isSubmitting}
                        className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50"
                      >
                        取消
                      </button>
                      <button
                        onClick={handleSubmit}
                        disabled={isSubmitting || !currentAccount}
                        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"
                      >
                        {isSubmitting && (
                          <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
                        )}
                        <span>{isSubmitting ? (isEditMode ? '更新中...' : '创建中...') : (isEditMode ? '更新密钥' : '创建密钥')}</span>
                      </button>
                    </div>
                  </div>
                )}
              </Dialog.Panel>
            </Transition.Child>
          </div>
        </div>
      </Dialog>
    </Transition>
  )
}

================================================
FILE: components/AutoDetectErrorAlert.tsx
================================================
import { Fragment } from 'react'
import { ExclamationTriangleIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import type { AutoDetectError, AutoDetectErrorProps } from '../utils/autoDetectUtils'
import { openLoginTab } from '../utils/autoDetectUtils'

export default function AutoDetectErrorAlert({ 
  error, 
  siteUrl, 
  onHelpClick, 
  onActionClick 
}: AutoDetectErrorProps) {
  const handleActionClick = () => {
    if (onActionClick) {
      onActionClick()
    } else if (error.type === 'unauthorized' && siteUrl) {
      // 默认行为:打开登录页面
      openLoginTab(siteUrl)
    }
  }

  const handleHelpClick = () => {
    if (onHelpClick) {
      onHelpClick()
    } else if (error.helpDocUrl) {
      // 默认行为:打开帮助文档
      chrome.tabs.create({ url: error.helpDocUrl })
    }
  }

  return (
    <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4">
      <div className="flex">
        <div className="flex-shrink-0">
          <ExclamationTriangleIcon className="h-4 w-4 text-amber-400" />
        </div>
        <div className="ml-2 flex-1">
          <p className="text-xs text-amber-700">{error.message}</p>
          
          {/* 操作按钮区域 */}
          {(error.actionText || error.helpDocUrl) && (
            <div className="mt-2 flex space-x-2">
              {/* 主要操作按钮 */}
              {error.actionText && (
                <button
                  type="button"
                  onClick={handleActionClick}
                  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"
                >
                  {error.actionText}
                </button>
              )}
              
              {/* 帮助文档按钮 */}
              {error.helpDocUrl && (
                <button
                  type="button"
                  onClick={handleHelpClick}
                  className="inline-flex items-center px-2 py-1 text-xs font-medium text-amber-600 hover:text-amber-800 transition-colors"
                >
                  <QuestionMarkCircleIcon className="w-3 h-3 mr-1" />
                  帮助文档
                </button>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

================================================
FILE: components/BalanceSection.tsx
================================================
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"
import { ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/outline"
import CountUp from "react-countup"
import { UI_CONSTANTS } from "../constants/ui"
import { getCurrencySymbol, formatTokenCount } from "../utils/formatters"
import { useTimeFormatter } from "../hooks/useTimeFormatter"
import Tooltip from "./Tooltip"

interface BalanceSectionProps {
  // 金额数据
  totalConsumption: { USD: number; CNY: number }
  totalBalance: { USD: number; CNY: number }
  todayTokens: { upload: number; download: number }
  
  // 状态
  currencyType: 'USD' | 'CNY'
  activeTab: 'consumption' | 'balance'
  isInitialLoad: boolean
  lastUpdateTime: Date
  
  // 动画相关
  prevTotalConsumption: { USD: number; CNY: number }
  
  // 事件处理
  onCurrencyToggle: () => void
  onTabChange: (index: number) => void
}

export default function BalanceSection({
  totalConsumption,
  totalBalance,
  todayTokens,
  currencyType,
  activeTab,
  isInitialLoad,
  lastUpdateTime,
  prevTotalConsumption,
  onCurrencyToggle,
  onTabChange
}: BalanceSectionProps) {
  const { formatRelativeTime, formatFullTime } = useTimeFormatter()
  
  return (
    <div className="px-6 py-6 bg-gradient-to-br from-blue-50/50 to-indigo-50/30">
      <div className="space-y-3">
        {/* 金额标签页 */}
        <div>
          <TabGroup selectedIndex={activeTab === 'consumption' ? 0 : 1} onChange={onTabChange}>
            <div className="flex justify-start mb-3">
              <TabList className="flex space-x-1 bg-gray-100 rounded-lg p-1">
                <Tab className={({ selected }) => 
                  `px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
                    selected 
                      ? 'bg-white text-gray-900 shadow-sm' 
                      : 'text-gray-500 hover:text-gray-700'
                  }`
                }>
                  今日消耗
                </Tab>
                <Tab className={({ selected }) => 
                  `px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
                    selected 
                      ? 'bg-white text-gray-900 shadow-sm' 
                      : 'text-gray-500 hover:text-gray-700'
                  }`
                }>
                  总余额
                </Tab>
              </TabList>
            </div>
            
            <TabPanels>
              <TabPanel>
                {/* 今日消耗面板 */}
                <div className="flex items-center space-x-1">
                  <button
                    onClick={onCurrencyToggle}
                    className="text-5xl font-bold text-gray-900 tracking-tight hover:text-blue-600 transition-colors cursor-pointer"
                    title={`点击切换到 ${currencyType === 'USD' ? '人民币' : '美元'}`}
                  >
                    {totalConsumption[currencyType] > 0 ? '-' : ''}{getCurrencySymbol(currencyType)}
                    <CountUp
                      start={isInitialLoad ? 0 : prevTotalConsumption[currencyType]}
                      end={totalConsumption[currencyType]}
                      duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.INITIAL_DURATION : UI_CONSTANTS.ANIMATION.UPDATE_DURATION}
                      decimals={2}
                      preserveValue
                    />
                  </button>
                </div>
              </TabPanel>
              
              <TabPanel>
                {/* 总余额面板 */}
                <div className="flex items-center space-x-1">
                  <button
                    onClick={onCurrencyToggle}
                    className="text-5xl font-bold text-gray-900 tracking-tight hover:text-blue-600 transition-colors cursor-pointer"
                    title={`点击切换到 ${currencyType === 'USD' ? '人民币' : '美元'}`}
                  >
                    {getCurrencySymbol(currencyType)}
                    <CountUp
                      start={isInitialLoad ? 0 : 0}
                      end={totalBalance[currencyType]}
                      duration={isInitialLoad ? UI_CONSTANTS.ANIMATION.INITIAL_DURATION : UI_CONSTANTS.ANIMATION.UPDATE_DURATION}
                      decimals={2}
                      preserveValue
                    />
                  </button>
                </div>
              </TabPanel>
            </TabPanels>
          </TabGroup>
        </div>
        
        {/* Token 统计信息 */}
        <div>
          <Tooltip
            content={
              <div>
                <div>提示: {todayTokens.upload.toLocaleString()} tokens</div>
                <div>补全: {todayTokens.download.toLocaleString()} tokens</div>
              </div>
            }
          >
            <div className="flex items-center space-x-3 cursor-help">
              <div className="flex items-center space-x-1">
                <ArrowUpIcon className="w-4 h-4 text-green-500" />
                <span className="font-medium text-gray-500">
                  {formatTokenCount(todayTokens.upload)}
                </span>
              </div>
              <div className="flex items-center space-x-1">
                <ArrowDownIcon className="w-4 h-4 text-blue-500" />
                <span className="font-medium text-gray-500">
                  {formatTokenCount(todayTokens.download)}
                </span>
              </div>
            </div>
          </Tooltip>
        </div>
      </div>
      
      {/* 最后更新时间 */}
      <div className="mt-4 pt-3 border-t border-gray-100">
        <div className="ml-2">
          <Tooltip content={formatFullTime(lastUpdateTime)}>
            <p className="text-xs text-gray-400 cursor-help">
              更新于 {formatRelativeTime(lastUpdateTime)}
            </p>
          </Tooltip>
        </div>
      </div>
    </div>
  )
}

================================================
FILE: components/CopyKeyDialog.tsx
================================================
import { Fragment, useState, useEffect } from "react"
import toast from 'react-hot-toast'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react"
import { 
  XMarkIcon, 
  KeyIcon, 
  DocumentDuplicateIcon, 
  ExclamationTriangleIcon,
  CheckIcon,
  ClockIcon,
  UserGroupIcon,
  ChevronDownIcon,
  ChevronRightIcon
} from "@heroicons/react/24/outline"
import { UI_CONSTANTS } from "../constants/ui"
import { fetchAccountTokens, type ApiToken } from "../services/apiService"
import type { DisplaySiteData } from "../types"

interface CopyKeyDialogProps {
  isOpen: boolean
  onClose: () => void
  account: DisplaySiteData | null
}

export default function CopyKeyDialog({ isOpen, onClose, account }: CopyKeyDialogProps) {
  const [tokens, setTokens] = useState<ApiToken[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [copiedKey, setCopiedKey] = useState<string | null>(null)
  const [expandedTokens, setExpandedTokens] = useState<Set<number>>(new Set())

  // 获取密钥列表
  const fetchTokens = async () => {
    if (!account) return

    setIsLoading(true)
    setError(null)
    
    try {
      // 使用 DisplaySiteData 中的 userId 字段
      const tokensResponse = await fetchAccountTokens(account.baseUrl, account.userId, account.token)
      
      // 确保返回的是数组
      if (Array.isArray(tokensResponse)) {
        setTokens(tokensResponse)
      } else {
        console.warn('Token response is not an array:', tokensResponse)
        setTokens([])
      }
    } catch (error) {
      console.error('获取密钥列表失败:', error)
      const errorMessage = error instanceof Error ? error.message : '未知错误'
      setError(`获取密钥列表失败: ${errorMessage}`)
    } finally {
      setIsLoading(false)
    }
  }

  // 当对话框打开时获取密钥列表
  useEffect(() => {
    if (isOpen && account) {
      fetchTokens()
    } else {
      // 关闭时重置状态
      setTokens([])
      setError(null)
      setCopiedKey(null)
      setExpandedTokens(new Set())
    }
  }, [isOpen, account])

  // 复制密钥到剪贴板
  const copyKey = async (key: string) => {
    try {
      // 检查key是否以"sk-"开头,如果不是则添加前缀
      const textToCopy = key.startsWith('sk-') ? key : 'sk-' + key;
      await navigator.clipboard.writeText(textToCopy);
      setCopiedKey(key);
      toast.success('密钥已复制到剪贴板');
      
      // 2秒后清除复制状态
      setTimeout(() => {
        setCopiedKey(null);
      }, 2000);
    } catch (error) {
      console.error('复制失败:', error);
      toast.error('复制失败,请手动复制');
    }
};


  // 切换密钥展开/折叠状态
  const toggleTokenExpansion = (tokenId: number) => {
    setExpandedTokens(prev => {
      const newSet = new Set(prev)
      if (newSet.has(tokenId)) {
        newSet.delete(tokenId)
      } else {
        newSet.add(tokenId)
      }
      return newSet
    })
  }

  // 格式化额度显示
  const formatQuota = (token: ApiToken) => {
    if (token.unlimited_quota || token.remain_quota < 0) {
      return '无限额度'
    }
    
    // 使用CONVERSION_FACTOR转换真实额度
    const realQuota = token.remain_quota / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR
    return `$${realQuota.toFixed(2)}`
  }

  // 格式化已用额度
  const formatUsedQuota = (token: ApiToken) => {
    const realUsedQuota = token.used_quota / UI_CONSTANTS.EXCHANGE_RATE.CONVERSION_FACTOR
    return `$${realUsedQuota.toFixed(2)}`
  }

  // 格式化时间
  const formatTime = (timestamp: number) => {
    if (timestamp <= 0) return '永不过期'
    return new Date(timestamp * 1000).toLocaleDateString('zh-CN')
  }

  // 获取组别徽章样式
  const getGroupBadgeStyle = (group: string) => {
    // 处理可能为空或未定义的 group
    const groupName = group || 'default'
    
    // 根据组别名称生成不同的颜色主题
    const hash = groupName.split('').reduce((a, b) => {
      a = ((a << 5) - a) + b.charCodeAt(0)
      return a & a
    }, 0)
    
    const colors = [
      'bg-blue-100 text-blue-800 border-blue-200',
      'bg-green-100 text-green-800 border-green-200', 
      'bg-purple-100 text-purple-800 border-purple-200',
      'bg-orange-100 text-orange-800 border-orange-200',
      'bg-pink-100 text-pink-800 border-pink-200',
      'bg-indigo-100 text-indigo-800 border-indigo-200',
      'bg-teal-100 text-teal-800 border-teal-200',
      'bg-yellow-100 text-yellow-800 border-yellow-200'
    ]
    
    return colors[Math.abs(hash) % colors.length]
  }

  // 获取状态徽章样式
  const getStatusBadgeStyle = (status: number) => {
    return status === 1 
      ? 'bg-green-100 text-green-800 border-green-200'
      : 'bg-red-100 text-red-800 border-red-200'
  }

  return (
    <Transition show={isOpen} as={Fragment}>
      <Dialog
        onClose={onClose}
        className="relative z-50"
      >
        {/* 背景遮罩动画 */}
        <TransitionChild
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black/30 backdrop-blur-sm" aria-hidden="true" />
        </TransitionChild>
        
        {/* 居中容器 */}
        <div className="fixed inset-0 flex items-center justify-center p-4">
          {/* 弹窗面板动画 */}
          <TransitionChild
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0 scale-95 translate-y-4"
            enterTo="opacity-100 scale-100 translate-y-0"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 scale-100 translate-y-0"
            leaveTo="opacity-0 scale-95 translate-y-4"
          >
            <DialogPanel className="w-full max-w-md bg-white rounded-lg shadow-xl transform transition-all max-h-[85vh] overflow-hidden flex flex-col">
              {/* 头部 */}
              <div className="flex items-center justify-between p-4 border-b border-gray-100">
                <div className="flex items-center space-x-3">
                  <div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-indigo-600 rounded-lg flex items-center justify-center">
                    <KeyIcon className="w-4 h-4 text-white" />
                  </div>
                  <div>
                    <DialogTitle className="text-lg font-semibold text-gray-900">
                      密钥列表
                    </DialogTitle>
                    <p className="text-xs text-gray-500 mt-0.5">
                      {account?.name}
                    </p>
                  </div>
                </div>
                <button
                  onClick={onClose}
                  className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
                >
                  <XMarkIcon className="w-4 h-4" />
                </button>
              </div>

              {/* 内容区域 */}
              <div className="flex-1 overflow-y-auto p-4">
                {isLoading ? (
                  <div className="flex flex-col items-center justify-center py-8">
                    <div className="w-8 h-8 border-2 border-purple-300 border-t-purple-600 rounded-full animate-spin mb-4" />
                    <p className="text-sm text-gray-500">正在获密钥列表...</p>
                  </div>
                ) : error ? (
                  <div className="bg-red-50 border border-red-200 rounded-lg p-4">
                    <div className="flex items-start">
                      <ExclamationTriangleIcon className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
                      <div className="ml-3">
                        <h3 className="text-sm font-medium text-red-800">获取失败</h3>
                        <p className="text-sm text-red-700 mt-1">{error}</p>
                        <button
                          onClick={fetchTokens}
                          className="mt-3 px-3 py-1.5 bg-red-100 text-red-800 text-xs rounded-lg hover:bg-red-200 transition-colors"
                        >
                          重试
                        </button>
                      </div>
                    </div>
                  </div>
                ) : !Array.isArray(tokens) || tokens.length === 0 ? (
                  <div className="text-center py-8">
                    <KeyIcon className="w-12 h-12 text-gray-300 mx-auto mb-4" />
                    <p className="text-gray-500 text-sm">暂无密钥数据</p>
                  </div>
                ) : (
                  <div className="space-y-3">
                    {Array.isArray(tokens) && tokens.map((token) => {
                      const isExpanded = expandedTokens.has(token.id)
                      
                      return (
                        <div
                          key={token.id}
                          className="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-sm transition-all duration-200"
                        >
                          {/* 头部:名称、组别徽章和展开/折叠按钮 */}
                          <div 
                            className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 transition-colors"
                            onClick={() => toggleTokenExpansion(token.id)}
                          >
                            <div className="flex-1 min-w-0 space-y-1.5">
                              <h4 className="font-medium text-gray-900 text-sm truncate">
                                {token.name}
                              </h4>
                              <div className="flex items-center space-x-1.5">
                                <UserGroupIcon className="w-3 h-3 text-gray-400" />
                                <span 
                                  className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${getGroupBadgeStyle(token.group || '')}`}
                                >
                                  {token.group || '默认组'}
                                </span>
                              </div>
                            </div>
                            
                            <div className="flex items-center space-x-2 ml-3">
                              {/* 状态徽章 */}
                              <span 
                                className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium border ${getStatusBadgeStyle(token.status)}`}
                              >
                                {token.status === 1 ? '启用' : '禁用'}
                              </span>
                              
                              {/* 展开/折叠图标 */}
                              {isExpanded ? (
                                <ChevronDownIcon className="w-4 h-4 text-gray-400" />
                              ) : (
                                <ChevronRightIcon className="w-4 h-4 text-gray-400" />
                              )}
                            </div>
                          </div>

                          {/* 可展开的详细信息区域 */}
                          {isExpanded && (
                            <div className="px-3 pb-3 border-t border-gray-100 bg-gray-50/30">
                              {/* 过期时间 */}
                              <div className="flex items-center space-x-1 text-xs text-gray-500 mb-3 pt-3">
                                <ClockIcon className="w-3 h-3" />
                                <span>过期时间: {formatTime(token.expired_time)}</span>
                              </div>

                              {/* 额度信息网格 */}
                              <div className="grid grid-cols-2 gap-2 mb-3">
                                <div className="bg-white rounded p-2 border border-gray-100">
                                  <div className="text-xs text-gray-500 mb-0.5">已用额度</div>
                                  <div className="text-sm font-semibold text-gray-900">
                                    {formatUsedQuota(token)}
                                  </div>
                                </div>
                                <div className="bg-white rounded p-2 border border-gray-100">
                                  <div className="text-xs text-gray-500 mb-0.5">剩余额度</div>
                                  <div className={`text-sm font-semibold ${
                                    token.unlimited_quota || token.remain_quota < 0 
                                      ? 'text-green-600' 
                                      : token.remain_quota < 1000000 
                                        ? 'text-orange-600' 
                                        : 'text-gray-900'
                                  }`}>
                                    {formatQuota(token)}
                                  </div>
                                </div>
                              </div>

                              {/* 密钥预览 */}
                              <div className="bg-white rounded p-2 border border-gray-100">
                                <div className="flex items-center justify-between mb-1">
                                  <span className="text-xs font-medium text-gray-500 uppercase tracking-wide">API 密钥</span>
                                  <button
                                    onClick={(e) => {
                                      e.stopPropagation()
                                      copyKey(token.key)
                                    }}
                                    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"
                                  >
                                    {copiedKey === token.key ? (
                                      <>
                                        <CheckIcon className="w-3 h-3" />
                                        <span>已复制</span>
                                      </>
                                    ) : (
                                      <>
                                        <DocumentDuplicateIcon className="w-3 h-3" />
                                        <span>复制</span>
                                      </>
                                    )}
                                  </button>
                                </div>
                                <div className="font-mono text-xs text-gray-700 bg-gray-50 px-2 py-1 rounded border border-gray-200 break-all">
                                  <span className="text-gray-900">{token.key.substring(0, 16)}</span>
                                  <span className="text-gray-400">{'•'.repeat(6)}</span>
                                  <span className="text-gray-900">{token.key.substring(token.key.length - 6)}</span>
                                </div>
                              </div>
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                )}
              </div>

              {/* 底部操作区 */}
              <div className="px-4 py-3 border-t border-gray-100 bg-gray-50/50">
                <div className="flex items-center justify-between">
                  <div className="flex items-center space-x-2">
                    {tokens.length > 0 && (
                      <div className="flex items-center space-x-1.5 text-xs text-gray-500">
                        <KeyIcon className="w-3 h-3" />
                        <span>共 {tokens.length} 个密钥</span>
                      </div>
                    )}
                  </div>
                  <button
                    onClick={onClose}
                    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"
                  >
                    关闭
                  </button>
                </div>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </Dialog>
    </Transition>
  )
}

================================================
FILE: components/DelAccountDialog.tsx
================================================
import { Fragment } from "react"
import toast from 'react-hot-toast'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react"
import { ExclamationTriangleIcon, XMarkIcon, TrashIcon } from "@heroicons/react/24/outline"
import { accountStorage } from "../services/accountStorage"
import type { DisplaySiteData } from "../types"

interface DelAccountDialogProps {
  isOpen: boolean
  onClose: () => void
  account: DisplaySiteData | null
  onDeleted: () => void
}

export default function DelAccountDialog({ isOpen, onClose, account, onDeleted }: DelAccountDialogProps) {
  const handleDelete = async () => {
    if (!account) {
      return
    }

    try {
      console.log('准备删除账号:', { id: account.id, name: account.name })
      
      await toast.promise(
        accountStorage.deleteAccount(account.id),
        {
          loading: `正在删除账号 ${account.name}...`,
          success: (success) => {
            if (success) {
              onDeleted()
              onClose()
              return `账号 ${account.name} 删除成功!`
            } else {
              throw new Error('删除失败')
            }
          },
          error: (err) => {
            const errorMsg = err instanceof Error ? err.message : '未知错误'
            return `删除失败: ${errorMsg}`
          },
        }
      )
    } catch (error) {
      console.error('删除账号失败:', error)
    }
  }

  return (
    <Transition show={isOpen} as={Fragment}>
      <Dialog
        onClose={onClose}
        className="relative z-50"
      >
        {/* 背景遮罩动画 */}
        <TransitionChild
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black/30 backdrop-blur-sm" aria-hidden="true" />
        </TransitionChild>
        
        {/* 居中容器 */}
        <div className="fixed inset-0 flex items-center justify-center p-4">
          {/* 弹窗面板动画 */}
          <TransitionChild
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0 scale-95 translate-y-4"
            enterTo="opacity-100 scale-100 translate-y-0"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 scale-100 translate-y-0"
            leaveTo="opacity-0 scale-95 translate-y-4"
          >
            <DialogPanel className="w-full max-w-sm bg-white rounded-lg shadow-xl transform transition-all">
              {/* 头部 */}
              <div className="flex items-center justify-between p-4 border-b border-gray-100">
                <div className="flex items-center space-x-3">
                  <div className="w-8 h-8 bg-gradient-to-r from-red-500 to-pink-600 rounded-lg flex items-center justify-center">
                    <TrashIcon className="w-4 h-4 text-white" />
                  </div>
                  <DialogTitle className="text-lg font-semibold text-gray-900">
                    删除账号
                  </DialogTitle>
                </div>
                <button
                  onClick={onClose}
                  className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
                >
                  <XMarkIcon className="w-5 h-5" />
                </button>
              </div>

              {/* 内容区域 */}
              <div className="p-4">
                {/* 警告图标和信息 */}
                <div className="flex items-start space-x-3 mb-4">
                  <div className="flex-shrink-0">
                    <ExclamationTriangleIcon className="w-6 h-6 text-red-500" />
                  </div>
                  <div className="flex-1">
                    <h3 className="text-sm font-medium text-gray-900 mb-2">
                      删除确认
                    </h3>
                    <p className="text-sm text-gray-500 mb-3">
                      您即将删除账号 <span className="font-medium text-gray-900">{account?.name}</span>。
                    </p>
                    <p className="text-sm text-gray-500">
                      请核对后确认是否删除此账号
                    </p>
                  </div>
                </div>

                {/* 账号信息显示 */}
                {account && (
                  <div className="bg-gray-50 rounded-lg p-3 mb-4">
                    <div className="text-sm">
                      <div className="flex justify-between items-center mb-1">
                        <span className="text-gray-500">站点名称:</span>
                        <span className="font-medium text-gray-900">{account.name}</span>
                      </div>
                      <div className="flex justify-between items-center mb-1">
                        <span className="text-gray-500">用户名:</span>
                        <span className="font-medium text-gray-900">{account.username}</span>
                      </div>
                      <div className="flex justify-between items-center">
                        <span className="text-gray-500">站点地址:</span>
                        <span className="font-medium text-gray-900 truncate ml-2 max-w-48" title={account.baseUrl}>
                          {account.baseUrl}
                        </span>
                      </div>
                    </div>
                  </div>
                )}

                {/* 按钮组 */}
                <div className="flex space-x-3">
                  <button
                    type="button"
                    onClick={onClose}
                    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"
                  >
                    取消
                  </button>
                  <button
                    type="button"
                    onClick={handleDelete}
                    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"
                  >
                    确认删除
                  </button>
                </div>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </Dialog>
    </Transition>
  )
}

================================================
FILE: components/EditAccountDialog.tsx
================================================
import { useState, useEffect, Fragment } from "react"
import toast from 'react-hot-toast'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react"
import { GlobeAltIcon, XMarkIcon, PencilIcon, UserIcon, KeyIcon, EyeIcon, EyeSlashIcon, CurrencyDollarIcon, SparklesIcon, CheckIcon, UsersIcon } from "@heroicons/react/24/outline"
import { accountStorage } from "../services/accountStorage"
import { autoDetectAccount, validateAndUpdateAccount, extractDomainPrefix, isValidExchangeRate } from "../services/accountOperations"
import AutoDetectErrorAlert from "./AutoDetectErrorAlert"
import type { AutoDetectError } from "../utils/autoDetectUtils"
import type { DisplaySiteData } from "../types"

interface EditAccountDialogProps {
  isOpen: boolean
  onClose: () => void
  account: DisplaySiteData | null
}

export default function EditAccountDialog({ isOpen, onClose, account }: EditAccountDialogProps) {
  const [url, setUrl] = useState("")
  const [isDetecting, setIsDetecting] = useState(false)
  const [siteName, setSiteName] = useState("")
  const [username, setUsername] = useState("")
  const [accessToken, setAccessToken] = useState("")
  const [userId, setUserId] = useState("")
  const [isDetected, setIsDetected] = useState(false)
  const [isSaving, setIsSaving] = useState(false)
  const [showAccessToken, setShowAccessToken] = useState(false)
  const [detectionError, setDetectionError] = useState<AutoDetectError | null>(null)
  const [showManualForm, setShowManualForm] = useState(true) // 编辑模式默认显示表单
  const [exchangeRate, setExchangeRate] = useState("")
  
  // 重置表单数据
  const resetForm = () => {
    setUrl("")
    setIsDetected(false)
    setSiteName("")
    setUsername("")
    setAccessToken("")
    setUserId("")
    setShowAccessToken(false)
    setDetectionError(null)
    setShowManualForm(true)
    setExchangeRate("")
  }

  // 加载账号数据到表单
  const loadAccountData = async (accountId: string) => {
    try {
      const siteAccount = await accountStorage.getAccountById(accountId)
      if (siteAccount) {
        setUrl(siteAccount.site_url)
        setSiteName(siteAccount.site_name)
        setUsername(siteAccount.account_info.username)
        setAccessToken(siteAccount.account_info.access_token)
        setUserId(siteAccount.account_info.id.toString())
        setExchangeRate(siteAccount.exchange_rate.toString())
      }
    } catch (error) {
      console.error('加载账号数据失败:', error)
    }
  }

  useEffect(() => {
    if (isOpen && account) {
      resetForm()
      loadAccountData(account.id)
    } else if (!isOpen) {
      resetForm()
    }
  }, [isOpen, account])

  const handleAutoDetect = async () => {
    if (!url.trim()) {
      return
    }

    setIsDetecting(true)
    setDetectionError(null)
    
    try {
      const result = await autoDetectAccount(url.trim())
      
      if (!result.success) {
        setDetectionError(result.detailedError || null)
        return
      }

      if (result.data) {
        // 更新表单数据
        setUsername(result.data.username)
        setAccessToken(result.data.accessToken)
        setUserId(result.data.userId)
        
        // 设置充值比例默认值
        if (result.data.exchangeRate) {
          setExchangeRate(result.data.exchangeRate.toString())
          console.log('获取到默认充值比例:', result.data.exchangeRate)
        } else {
          console.log('未获取到默认充值比例,保持当前值')
        }
        
        setIsDetected(true)
        
        console.log('自动识别成功:', { 
          username: result.data.username, 
          siteName, 
          exchangeRate: result.data.exchangeRate 
        })
      }
    } catch (error) {
      console.error('自动识别失败:', error)
      const errorMessage = error instanceof Error ? error.message : '未知错误'
      // 使用通用错误处理
      setDetectionError({
        type: 'unknown' as any,
        message: `自动识别失败: ${errorMessage}`,
        helpDocUrl: '#'
      })
    } finally {
      setIsDetecting(false)
    }
  }

  const handleSaveAccount = async () => {
    if (!account) {
      toast.error('账号信息错误')
      return
    }

    setIsSaving(true)
    
    try {
      await toast.promise(
        validateAndUpdateAccount(
          account.id,
          url.trim(),
          siteName.trim(),
          username.trim(),
          accessToken.trim(),
          userId.trim(),
          exchangeRate
        ),
        {
          loading: '正在保存更改...',
          success: (result) => {
            if (result.success) {
              onClose()
              return `账号 ${siteName} 更新成功!`
            } else {
              throw new Error(result.error || '更新失败')
            }
          },
          error: (err) => {
            const errorMsg = err.message || '更新失败'
            return `更新失败: ${errorMsg}`
          },
        }
      )
    } catch (error) {
      console.error('更新账号失败:', error)
    } finally {
      setIsSaving(false)
    }
  }

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (isDetected || showManualForm) {
      handleSaveAccount()
    } else {
      handleAutoDetect()
    }
  }

  return (
    <Transition show={isOpen} as={Fragment}>
      <Dialog
        onClose={onClose}
        className="relative z-50"
      >
        {/* 背景遮罩动画 */}
        <TransitionChild
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black/30 backdrop-blur-sm" aria-hidden="true" />
        </TransitionChild>
        
        {/* 居中容器 - 针对插件优化 */}
        <div className="fixed inset-0 flex items-center justify-center p-2">
          {/* 弹窗面板动画 */}
          <TransitionChild
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0 scale-95 translate-y-4"
            enterTo="opacity-100 scale-100 translate-y-0"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 scale-100 translate-y-0"
            leaveTo="opacity-0 scale-95 translate-y-4"
          >
            <DialogPanel className="w-full max-w-sm bg-white rounded-lg shadow-xl transform transition-all max-h-[90vh] overflow-y-auto">
              {/* 头部 */}
              <div className="flex items-center justify-between p-4 border-b border-gray-100">
                <div className="flex items-center space-x-3">
                  <div className="w-8 h-8 bg-gradient-to-r from-green-500 to-emerald-600 rounded-lg flex items-center justify-center">
                    <PencilIcon className="w-4 h-4 text-white" />
                  </div>
                  <DialogTitle className="text-lg font-semibold text-gray-900">
                    编辑账号
                  </DialogTitle>
                </div>
                <button
                  onClick={onClose}
                  className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
                >
                  <XMarkIcon className="w-5 h-5" />
                </button>
              </div>

              {/* 内容区域 */}
              <div className="p-4">
                <form onSubmit={handleSubmit} className="space-y-6">
                  {/* 识别错误提示 */}
                  {detectionError && (
                    <AutoDetectErrorAlert 
                      error={detectionError}
                      siteUrl={url}
                    />
                  )}

                  {/* URL 输入框 */}
                  <div>
                    <label className="block text-sm font-medium text-gray-700 mb-2">
                      站点地址
                    </label>
                    <div className="relative">
                      <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                        <GlobeAltIcon className="h-5 w-5 text-gray-400" />
                      </div>
                      <input
                        type="url"
                        value={url}
                        onChange={(e) => {
                          const inputUrl = e.target.value
                          
                          // 当用户输入 URL 时,提取协议和主机部分
                          if (inputUrl.trim()) {
                            try {
                              const urlObj = new URL(inputUrl)
                              // 只保留协议和主机部分,不带路径
                              const baseUrl = `${urlObj.protocol}//${urlObj.host}`
                              setUrl(baseUrl)
                              
                              // 自动更新站点名称
                              const domainPrefix = extractDomainPrefix(urlObj.hostname)
                              setSiteName(domainPrefix)
                            } catch (error) {
                              // 如果 URL 格式不完整,先保存用户输入,但尝试提取域名
                              setUrl(inputUrl)
                              const match = inputUrl.match(/\/\/([^\/]+)/)
                              if (match) {
                                const domainPrefix = extractDomainPrefix(match[1])
                                setSiteName(domainPrefix)
                              }
                            }
                          } else {
                            setUrl("")
                            setSiteName("")
                          }
                        }}
                        placeholder="https://example.com"
                        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"
                        required
                        disabled={isDetected}
                      />
                      {url && (
                        <button
                          type="button"
                          onClick={() => setUrl('')}
                          className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
                          disabled={isDetected}
                        >
                          <XMarkIcon className="h-4 w-4" />
                        </button>
                      )}
                    </div>
                    <p className="mt-2 text-xs text-gray-500">
                      请输入 One API 或 New API 站点的完整地址
                    </p>
                  </div>

                  {/* 账号信息表单 */}
                  <div className="space-y-6">
                    {/* 网站名称 */}
                    <div>
                      <label className="block text-sm font-medium text-gray-700 mb-2">
                        网站名称
                      </label>
                      <div className="relative">
                        <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                          <GlobeAltIcon className="h-5 w-5 text-gray-400" />
                        </div>
                        <input
                          type="text"
                          value={siteName}
                          onChange={(e) => setSiteName(e.target.value)}
                          placeholder="example.com"
                          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"
                          required
                        />
                      </div>
                    </div>

                    {/* 用户名 */}
                    <div>
                      <label className="block text-sm font-medium text-gray-700 mb-2">
                        用户名
                      </label>
                      <div className="relative">
                        <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                          <UserIcon className="h-5 w-5 text-gray-400" />
                        </div>
                        <input
                          type="text"
                          value={username}
                          onChange={(e) => setUsername(e.target.value)}
                          placeholder="用户名"
                          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"
                          required
                        />
                      </div>
                    </div>

                    {/* 用户 ID */}
                    <div>
                      <label className="block text-sm font-medium text-gray-700 mb-2">
                        用户 ID
                      </label>
                      <div className="relative">
                        <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                          <span className="text-gray-400 font-mono text-sm">#</span>
                        </div>
                        <input
                          type="number"
                          value={userId}
                          onChange={(e) => setUserId(e.target.value)}
                          placeholder="用户 ID (数字)"
                          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"
                          required
                        />
                      </div>
                    </div>

                    {/* 访问令牌 */}
                    <div>
                      <label className="block text-sm font-medium text-gray-700 mb-2">
                        访问令牌
                      </label>
                      <div className="relative">
                        <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                          <KeyIcon className="h-5 w-5 text-gray-400" />
                        </div>
                        <input
                          type={showAccessToken ? "text" : "password"}
                          value={accessToken}
                          onChange={(e) => setAccessToken(e.target.value)}
                          placeholder="访问令牌"
                          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"
                          required
                        />
                        <button
                          type="button"
                          onClick={() => setShowAccessToken(!showAccessToken)}
                          className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
                        >
                          {showAccessToken ? (
                            <EyeSlashIcon className="h-4 w-4" />
                          ) : (
                            <EyeIcon className="h-4 w-4" />
                          )}
                        </button>
                      </div>
                    </div>

                    {/* 充值金额比例 */}
                    <div>
                      <label className="block text-sm font-medium text-gray-700 mb-2">
                        充值金额比例 (CNY/USD)
                      </label>
                      <div className="relative">
                        <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                          <CurrencyDollarIcon className="h-5 w-5 text-gray-400" />
                        </div>
                        <input
                          type="number"
                          step="0.1"
                          min="0.1"
                          max="100"
                          value={exchangeRate}
                          onChange={(e) => setExchangeRate(e.target.value)}
                          placeholder="请输入充值比例"
                          className={`block w-full pl-10 py-3 border rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 transition-colors ${
                            isValidExchangeRate(exchangeRate) 
                              ? 'border-gray-200 focus:ring-green-500 focus:border-transparent' 
                              : 'border-red-300 focus:ring-red-500 focus:border-red-500'
                          }`}
                          required
                        />
                        <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
                          <span className="text-sm text-gray-500">CNY</span>
                        </div>
                      </div>
                      <p className="mt-1 text-xs text-gray-500">
                        表示充值 1 美元需要多少人民币
                      </p>
                      {!isValidExchangeRate(exchangeRate) && exchangeRate && (
                        <p className="mt-1 text-xs text-red-600">
                          请输入有效的汇率 (0.1 - 100)
                        </p>
                      )}
                    </div>
                  </div>

                  {/* 按钮组 */}
                  <div className="flex space-x-3 pt-2">
                    <button
                      type="button"
                      onClick={onClose}
                      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"
                    >
                      取消
                    </button>
                    
                    {/* 重新识别按钮 */}
                    {!isDetected && (
                      <button
                        type="button"
                        onClick={handleAutoDetect}
                        disabled={!url.trim() || isDetecting}
                        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"
                      >
                        {isDetecting ? (
                          <>
                            <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
                            <span>识别中...</span>
                          </>
                        ) : (
                          <>
                            <SparklesIcon className="w-4 h-4" />
                            <span>重新识别</span>
                          </>
                        )}
                      </button>
                    )}
                    
                    {/* 保存按钮 */}
                    <button
                      type="submit"
                      disabled={!siteName.trim() || !username.trim() || !accessToken.trim() || !userId.trim() || !isValidExchangeRate(exchangeRate) || isSaving}
                      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"
                    >
                      {isSaving ? (
                        <>
                          <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
                          <span>保存中...</span>
                        </>
                      ) : (
                        <>
                          <CheckIcon className="w-4 h-4" />
                          <span>保存更改</span>
                        </>
                      )}
                    </button>
                  </div>
                </form>
              </div>

              {/* 提示信息 */}
              <div className="px-4 pb-4">
                <div className="bg-green-50 border border-green-100 rounded-lg p-3">
                  <div className="flex">
                    <div className="flex-shrink-0">
                      <UsersIcon className="h-5 w-5 text-green-400" />
                    </div>
                    <div className="ml-3">
                      <h3 className="text-xs font-medium text-green-800">
                        编辑账号信息
                      </h3>
                      <div className="mt-1 text-xs text-green-700">
                        <p>
                          修改账号信息后,系统会重新验证并获取最新的余额数据。
                        </p>
                        <p>
                          如果站点信息有变化,建议点击"重新识别"按钮(需要在目标站点先自行登录)
                        </p>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </Dialog>
    </Transition>
  )
}

================================================
FILE: components/HeaderSection.tsx
================================================
import { ArrowsPointingOutIcon, Cog6ToothIcon, ArrowPathIcon } from "@heroicons/react/24/outline"
import { UI_CONSTANTS } from "../constants/ui"
import Tooltip from "./Tooltip"
import iconImage from "../assets/icon.png"

interface HeaderSectionProps {
  isRefreshing: boolean
  onRefresh: () => void
  onOpenTab: () => void
  onOpenSettings: () => void
}

export default function HeaderSection({ 
  isRefreshing, 
  onRefresh, 
  onOpenTab,
  onOpenSettings
}: HeaderSectionProps) {
  return (
    <div className="flex items-center justify-between px-5 py-4 bg-white border-b border-gray-100 flex-shrink-0">
      <div className="flex items-center space-x-3">
        <img 
          src={iconImage} 
          alt="One API Hub" 
          className="w-7 h-7 rounded-lg shadow-sm"
        />
        <div className="flex flex-col">
          <span className="font-semibold text-gray-900">One API Hub</span>
          <span className="text-xs text-gray-500">一键管理所有AI中转站</span>
        </div>
      </div>
      
      <div className="flex items-center space-x-2">
        <Tooltip content="刷新数据">
          <button
            onClick={onRefresh}
            disabled={isRefreshing}
            className={`${UI_CONSTANTS.STYLES.BUTTON.ICON} ${isRefreshing ? 'animate-spin' : ''}`}
            title="刷新数据"
          >
            <ArrowPathIcon className="w-4 h-4" />
          </button>
        </Tooltip>
        <button
          onClick={onOpenTab}
          className={UI_CONSTANTS.STYLES.BUTTON.ICON}
          title="打开完整管理页面"
        >
          <ArrowsPointingOutIcon className="w-4 h-4" />
        </button>
        <button
          onClick={onOpenSettings}
          className={UI_CONSTANTS.STYLES.BUTTON.ICON}
          title="设置"
        >
          <Cog6ToothIcon className="w-4 h-4" />
        </button>
      </div>
    </div>
  )
}

================================================
FILE: components/ModelItem.tsx
================================================
/**
 * 模型列表项组件
 */

import React, { useState } from 'react'
import { 
  DocumentDuplicateIcon, 
  ChevronDownIcon, 
  ChevronUpIcon,
  TagIcon,
  CurrencyDollarIcon,
  ServerIcon
} from '@heroicons/react/24/outline'
import toast from 'react-hot-toast'
import type { ModelPricing } from '../services/apiService'
import type { CalculatedPrice } from '../utils/modelPricing'
import { 
  getProviderConfig, 
  type ProviderType 
} from '../utils/modelProviders'
import { 
  formatPrice, 
  formatPriceCompact, 
  getBillingModeText, 
  getBillingModeStyle,
  getEndpointTypesText 
} from '../utils/modelPricing'

interface ModelItemProps {
  model: ModelPricing
  calculatedPrice: CalculatedPrice
  exchangeRate: number
  showRealPrice: boolean // 是否以真实充值金额展示
  showRatioColumn: boolean // 是否显示倍率列
  showEndpointTypes: boolean // 是否显示可用端点类型
  userGroup: string
  onGroupClick?: (group: string) => void // 新增:点击分组时的回调函数
  availableGroups?: string[] // 新增:用户的所有可用分组列表
  isAllGroupsMode?: boolean // 新增:是否为"所有分组"模式
}

export default function ModelItem({
  model,
  calculatedPrice,
  exchangeRate,
  showRealPrice,
  showRatioColumn,
  showEndpointTypes,
  userGroup,
  onGroupClick,
  availableGroups = [],
  isAllGroupsMode = false
}: ModelItemProps) {
  const [isExpanded, setIsExpanded] = useState(false)
  
  // 获取厂商配置
  const providerConfig = getProviderConfig(model.model_name)
  const IconComponent = providerConfig.icon
  
  // 获取计费模式样式
  const billingStyle = getBillingModeStyle(model.quota_type)
  
  // 检查模型是否对当前用户分组可用
  const isAvailableForUser = isAllGroupsMode 
    ? availableGroups.some(group => model.enable_groups.includes(group)) // 所有分组模式:任何一个用户分组可用即可
    : model.enable_groups.includes(userGroup) // 特定分组模式:必须该分组可用
  
  // 复制模型名称
  const handleCopyModelName = async () => {
    try {
      await navigator.clipboard.writeText(model.model_name)
      toast.success('模型名称已复制')
    } catch (error) {
      toast.error('复制失败')
    }
  }
  
  return (
    <div className={`border rounded-lg transition-all duration-200 ${
      isAvailableForUser 
        ? 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm' 
        : 'border-gray-100 bg-gray-50'
    }`}>
      {/* 主要信息行 */}
      <div className="p-4">
        <div className="flex items-start justify-between">
          {/* 左侧:模型名称和基本信息 */}
          <div className="flex-1 min-w-0">
            <div className="flex items-center space-x-3 mb-2">
              {/* 厂商图标 */}
              <div className={`p-1.5 rounded-lg ${providerConfig.bgColor}`}>
                <IconComponent className={`w-4 h-4 ${providerConfig.color}`} />
              </div>
              
              {/* 模型名称 */}
              <div className="flex items-center space-x-2 min-w-0">
                <h3 className={`text-lg font-semibold ${
                  isAvailableForUser ? 'text-gray-900' : 'text-gray-500'
                }`}>
                  {model.model_name}
                </h3>
                
                {/* 复制按钮 */}
                <button
                  onClick={handleCopyModelName}
                  className="p-1 hover:bg-gray-100 rounded transition-colors"
                  title="复制模型名称"
                >
                  <DocumentDuplicateIcon className="w-3 h-3 text-gray-400" />
                </button>
              </div>
              
              {/* 计费模式标签 */}
              <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${billingStyle.color} ${billingStyle.bgColor}`}>
                {getBillingModeText(model.quota_type)}
              </span>
              
              {/* 可用状态标签 */}
              <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
                isAvailableForUser 
                  ? 'bg-green-100 text-green-800' 
                  : 'bg-gray-100 text-gray-600'
              }`}>
                {isAvailableForUser ? '可用' : '不可用'}
              </span>
            </div>
            
            {/* 模型描述 */}
            {model.model_description && (
              <div className="mb-2">
                <p className={`text-sm leading-relaxed ${
                  isAvailableForUser ? 'text-gray-600' : 'text-gray-400'
                } overflow-hidden`} 
                style={{
                  display: '-webkit-box',
                  WebkitLineClamp: 2,
                  WebkitBoxOrient: 'vertical'
                }}
                title={model.model_description}>
                  {model.model_description}
                </p>
              </div>
            )}
            
            {/* 价格信息 */}
            <div className="mt-2">
              {model.quota_type === 0 ? (
                // 按量计费 - 横向并排显示价格
                <div className="flex items-center gap-6">
                  {/* 输入价格 */}
                  <div className="flex items-center space-x-2">
                    <span className="text-sm text-gray-600">输入:</span>
                    <span className={`text-sm ${
                      isAvailableForUser ? 'text-blue-600' : 'text-gray-500'
                    }`}>
                      {showRealPrice 
                        ? `${formatPriceCompact(calculatedPrice.inputCNY, 'CNY')}/M`
                        : `${formatPriceCompact(calculatedPrice.inputUSD, 'USD')}/M`
                      }
                    </span>
                  </div>
                  
                  {/* 输出价格 */}
                  <div className="flex items-center space-x-2">
                    <span className="text-sm text-gray-600">输出:</span>
                    <span className={`text-sm ${
                      isAvailableForUser ? 'text-green-600' : 'text-gray-500'
                    }`}>
                      {showRealPrice 
                        ? `${formatPriceCompact(calculatedPrice.outputCNY, 'CNY')}/M`
                        : `${formatPriceCompact(calculatedPrice.outputUSD, 'USD')}/M`
                      }
                    </span>
                  </div>
                  
                  {/* 倍率显示 */}
                  {showRatioColumn && (
                    <div className="flex items-center space-x-2">
                      <span className="text-sm text-gray-500">倍率:</span>
                      <span className={`text-sm font-medium ${
                        isAvailableForUser ? 'text-gray-900' : 'text-gray-500'
                      }`}>
                        {model.model_ratio}x
                      </span>
                    </div>
                  )}
                </div>
              ) : (
                // 按次计费
                <div className="flex items-center space-x-2">
                  <span className="text-sm text-gray-600">每次调用:</span>
                  <span className={`text-sm ${
                    isAvailableForUser ? 'text-purple-600' : 'text-gray-500'
                  }`}>
                    {showRealPrice 
                      ? formatPriceCompact((calculatedPrice.perCallPrice || 0) * exchangeRate, 'CNY')
                      : formatPriceCompact(calculatedPrice.perCallPrice || 0, 'USD')
                    }
                  </span>
                </div>
              )}
            </div>
          </div>
          
          {/* 右侧:展开/收起按钮 */}
          <button
            onClick={() => setIsExpanded(!isExpanded)}
            className="ml-4 p-2 hover:bg-gray-100 rounded-lg transition-colors"
            title={isExpanded ? '收起详细信息' : '展开详细信息'}
          >
            {isExpanded ? (
              <ChevronUpIcon className="w-4 h-4 text-gray-400" />
            ) : (
              <ChevronDownIcon className="w-4 h-4 text-gray-400" />
            )}
          </button>
        </div>
      </div>
      
      {/* 展开的详细信息 */}
      {isExpanded && (
        <div className="border-t border-gray-100 px-4 py-3">
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
            {/* 可用分组 */}
            <div>
              <div className="flex items-center space-x-2 mb-2">
                <TagIcon className="w-4 h-4 text-gray-400" />
                <span className="font-medium text-gray-700">可用分组</span>
              </div>
              <div className="flex flex-wrap gap-1">
                {model.enable_groups.map((group, index) => {
                  const isCurrentGroup = group === userGroup
                  const isClickable = onGroupClick && !isCurrentGroup
                  
                  return (
                    <span 
                      key={index}
                      onClick={isClickable ? () => onGroupClick(group) : undefined}
                      className={`inline-flex items-center px-2 py-1 rounded text-xs cursor-pointer transition-colors ${
                        isCurrentGroup
                          ? 'bg-blue-100 text-blue-800 font-medium' 
                          : isClickable
                          ? 'bg-gray-100 text-gray-600 hover:bg-blue-50 hover:text-blue-700' 
                          : 'bg-gray-100 text-gray-600'
                      }`}
                      title={isClickable ? `点击切换到 ${group} 分组` : undefined}
                    >
                      {isCurrentGroup && <TagIcon className="w-3 h-3 mr-1" />}
                      {group}
                    </span>
                  )
                })}
              </div>
            </div>
            
            {/* 可用端点类型 */}
            {showEndpointTypes && (
              <div>
                <div className="flex items-center space-x-2 mb-2">
                  <ServerIcon className="w-4 h-4 text-gray-400" />
                  <span className="font-medium text-gray-700">端点类型</span>
                </div>
                <div className="text-gray-600">
                  {getEndpointTypesText(model.supported_endpoint_types)}
                </div>
              </div>
            )}
            
            {/* 详细定价信息(仅按量计费模型) */}
            {model.quota_type === 0 && (
              <div className="md:col-span-2">
                <div className="flex items-center space-x-2 mb-2">
                  <CurrencyDollarIcon className="w-4 h-4 text-gray-400" />
                  <span className="font-medium text-gray-700">详细定价</span>
                </div>
                <div className="grid grid-cols-2 gap-4 text-xs">
                  <div className="space-y-1">
                    <div className="text-gray-500">输入(1M tokens)</div>
                    <div className="font-medium">
                      USD: {formatPrice(calculatedPrice.inputUSD, 'USD')}
                    </div>
                    <div className="font-medium">
                      CNY: {formatPrice(calculatedPrice.inputCNY, 'CNY')}
                    </div>
                  </div>
                  <div className="space-y-1">
                    <div className="text-gray-500">输出(1M tokens)</div>
                    <div className="font-medium">
                      USD: {formatPrice(calculatedPrice.outputUSD, 'USD')}
                    </div>
                    <div className="font-medium">
                      CNY: {formatPrice(calculatedPrice.outputCNY, 'CNY')}
                    </div>
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  )
}

================================================
FILE: components/Tooltip.tsx
================================================
import { useState, useRef, useEffect } from "react"
import type { ReactNode } from "react"

interface TooltipProps {
  content: ReactNode
  children: ReactNode
  position?: 'top' | 'bottom' | 'left' | 'right' | 'auto'
  className?: string
  delay?: number
}

export default function Tooltip({ 
  content, 
  children, 
  position = 'auto', 
  className = '',
  delay = 0 
}: TooltipProps) {
  const [showTooltip, setShowTooltip] = useState(false)
  const [actualPosition, setActualPosition] = useState<'top' | 'bottom' | 'left' | 'right'>('top')
  const containerRef = useRef<HTMLDivElement>(null)
  const tooltipRef = useRef<HTMLDivElement>(null)

  const handleMouseEnter = () => {
    if (delay > 0) {
      setTimeout(() => setShowTooltip(true), delay)
    } else {
      setShowTooltip(true)
    }
  }

  const handleMouseLeave = () => {
    setShowTooltip(false)
  }

  // 检测最佳位置
  useEffect(() => {
    if (showTooltip && position === 'auto' && containerRef.current && tooltipRef.current) {
      const container = containerRef.current
      const tooltip = tooltipRef.current
      const containerRect = container.getBoundingClientRect()
      const tooltipRect = tooltip.getBoundingClientRect()
      
      // 插件窗口的边界(假设宽度为384px,高度为600px)
      const windowWidth = 384
      const windowHeight = 600
      
      let bestPosition: 'top' | 'bottom' | 'left' | 'right' = 'top'
      
      // 检查是否有足够空间显示在上方
      if (containerRect.top > tooltipRect.height + 10) {
        bestPosition = 'top'
      }
      // 检查是否有足够空间显示在下方
      else if (containerRect.bottom + tooltipRect.height + 10 < windowHeight) {
        bestPosition = 'bottom'
      }
      // 检查是否有足够空间显示在左侧
      else if (containerRect.left > tooltipRect.width + 10) {
        bestPosition = 'left'
      }
      // 检查是否有足够空间显示在右侧
      else if (containerRect.right + tooltipRect.width + 10 < windowWidth) {
        bestPosition = 'right'
      }
      // 默认显示在上方,即使空间不足
      else {
        bestPosition = 'top'
      }
      
      setActualPosition(bestPosition)
    } else if (position !== 'auto') {
      setActualPosition(position)
    }
  }, [showTooltip, position])

  const getPositionClasses = () => {
    switch (actualPosition) {
      case 'top':
        return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2'
      case 'bottom':
        return 'top-full left-1/2 transform -translate-x-1/2 mt-2'
      case 'left':
        return 'right-full top-1/2 transform -translate-y-1/2 mr-2'
      case 'right':
        return 'left-full top-1/2 transform -translate-y-1/2 ml-2'
      default:
        return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2'
    }
  }

  const getArrowClasses = () => {
    switch (actualPosition) {
      case 'top':
        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'
      case 'bottom':
        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'
      case 'left':
        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'
      case 'right':
        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'
      default:
        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'
    }
  }

  return (
    <div className="relative w-fit" ref={containerRef}>
      <div
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
      >
        {children}
      </div>
      
      <div 
        ref={tooltipRef}
        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}`}
      >
        {content}
        <div className={getArrowClasses()}></div>
      </div>
    </div>
  )
}

================================================
FILE: constants/ui.ts
================================================
/**
 * UI 相关常量定义
 */

export const UI_CONSTANTS = {
  // 弹窗尺寸
  POPUP: {
    WIDTH: 'w-96',
    HEIGHT: 'h-[600px]',
    MAX_HEIGHT: 'max-h-[90vh]'
  },

  // 动画配置
  ANIMATION: {
    INITIAL_DURATION: 1.5,
    UPDATE_DURATION: 0.8,
    FAST_DURATION: 0.6,
    SLOW_DURATION: 1.0
  },

  // 更新间隔
  UPDATE_INTERVAL: 30000, // 30秒

  // 排序相关
  SORT: {
    DEFAULT_FIELD: 'balance' as const,
    DEFAULT_ORDER: 'desc' as const
  },

  // Token 格式化阈值
  TOKEN: {
    MILLION_THRESHOLD: 1000000,
    THOUSAND_THRESHOLD: 1000
  },

  // 汇率相关
  EXCHANGE_RATE: {
    DEFAULT: 7.2,
    CONVERSION_FACTOR: 500000 // USD to quota conversion
  },

  // 样式类名
  STYLES: {
    // 按钮样式
    BUTTON: {
      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',
      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',
      ICON: 'p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-all duration-200'
    },
    
    // 状态指示器
    STATUS_INDICATOR: {
      HEALTHY: 'bg-green-500',
      ERROR: 'bg-red-500',
      WARNING: 'bg-yellow-500',
      UNKNOWN: 'bg-gray-400'
    },

    // 文本颜色
    TEXT: {
      PRIMARY: 'text-gray-900',
      SECONDARY: 'text-gray-500',
      SUCCESS: 'text-green-500',
      ERROR: 'text-red-500',
      WARNING: 'text-yellow-500'
    },

    // 输入框
    INPUT: {
      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',
      WITH_ICON: 'pl-10'
    }
  }
} as const

export const CURRENCY_SYMBOLS = {
  USD: '$',
  CNY: '¥'
} as const

export const HEALTH_STATUS_MAP = {
  healthy: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.HEALTHY, text: '正常' },
  error: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.ERROR, text: '错误' },
  warning: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.WARNING, text: '警告' },
  unknown: { color: UI_CONSTANTS.STYLES.STATUS_INDICATOR.UNKNOWN, text: '未知' }
} as const

================================================
FILE: content.ts
================================================
import type { PlasmoCSConfig } from "plasmo"

import { fetchUserInfo } from "~services/apiService"

export const config: PlasmoCSConfig = {
  matches: ["<all_urls>"],
  all_frames: false
}

// 监听来自 popup 和 background 的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "getLocalStorage") {
    try {
      const { key } = request

      if (key) {
        // 读取特定键
        const value = localStorage.getItem(key)
        sendResponse({ success: true, data: { [key]: value } })
      } else {
        // 读取所有 localStorage 数据
        const localStorage = window.localStorage
        const data = {}

        for (let i = 0; i < localStorage.length; i++) {
          const storageKey = localStorage.key(i)
          if (storageKey) {
            data[storageKey] = localStorage.getItem(storageKey)
          }
        }

        sendResponse({ success: true, data })
      }
    } catch (error) {
      sendResponse({ success: false, error: error.message })
    }
    return true // 保持消息通道开放
  }

  if (request.action === "getUserFromLocalStorage") {
    ;(async () => {
      try {
        // 所有异步逻辑
        const userStr = localStorage.getItem("user")
        let user = userStr
          ? JSON.parse(userStr)
          : await fetchUserInfo(request.url)

        if (!user || !user.id) {
          sendResponse({
            success: false,
            error: "未找到用户信息,请确保已登录"
          })
          return
        }

        sendResponse({ success: true, data: { userId: user.id, user } })
      } catch (e) {
        sendResponse({ success: false, error: e.message })
      }
    })()
    return true
  }

  if (request.action === "waitAndGetUserInfo") {
    // 新增:等待页面完全加载后获取用户信息
    waitForUserInfo()
      .then((userInfo) => {
        sendResponse({ success: true, data: userInfo })
      })
      .catch((error) => {
        sendResponse({ success: false, error: error.message })
      })
    return true
  }
})

// 等待用户信息可用
async function waitForUserInfo(
  maxWaitTime = 5000
): Promise<{ userId: string; user: any }> {
  const startTime = Date.now()

  while (Date.now() - startTime < maxWaitTime) {
    try {
      const userStr = localStorage.getItem("user")
      if (userStr) {
        const user = JSON.parse(userStr)
        if (user.id) {
          return { userId: user.id, user }
        }
      }
    } catch (error) {
      // 继续等待
    }

    // 等待 100ms 后重试
    await new Promise((resolve) => setTimeout(resolve, 100))
  }

  throw new Error("等待用户信息超时,请确保已登录")
}


================================================
FILE: debug/testStorage.ts
================================================
import { accountStorage } from "../services/accountStorage";

/**
 * 存储功能测试脚本
 */
export const testStorageFunction = async () => {
  console.log('=== 开始测试存储功能 ===');

  try {
    // 测试添加账号
    const testAccount = {
      emoji: "",
      site_name: "测试站点",
      site_url: "https://test.example.com",
      health_status: "unknown" as const,
      exchange_rate: 7.2,
      account_info: {
        access_token: "sk-test-1234567890",
        username: "test@example.com",
        quota: 100.0,
        today_prompt_tokens: 0,
        today_completion_tokens: 0,
        today_quota_consumption: 0,
        today_requests_count: 0
      },
      last_sync_time: Date.now()
    };

    console.log('1. 测试添加账号...');
    const accountId = await accountStorage.addAccount(testAccount);
    console.log('账号添加成功,ID:', accountId);

    // 测试获取所有账号
    console.log('2. 测试获取所有账号...');
    const allAccounts = await accountStorage.getAllAccounts();
    console.log('获取到账号数量:', allAccounts.length);
    console.log('账号列表:', allAccounts.map(acc => ({
      id: acc.id,
      name: acc.site_name,
      username: acc.account_info.username
    })));

    // 测试获取单个账号
    console.log('3. 测试获取单个账号...');
    const singleAccount = await accountStorage.getAccountById(accountId);
    console.log('获取单个账号成功:', singleAccount ? singleAccount.site_name : '未找到');

    // 测试数据导出
    console.log('4. 测试数据导出...');
    const exportedData = await accountStorage.exportData();
    console.log('导出数据:', {
      accountCount: exportedData.accounts.length,
      lastUpdated: new Date(exportedData.last_updated).toLocaleString()
    });

    console.log('=== 所有测试完成 ===');
    return true;
  } catch (error) {
    console.error('测试失败:', error);
    return false;
  }
};

// 在浏览器环境中暴露测试函数
if (typeof window !== 'undefined') {
  (window as any).testStorageFunction = testStorageFunction;
  console.log('测试函数已挂载到 window.testStorageFunction,在控制台运行 testStorageFunction() 进行测试');
}

================================================
FILE: docs/docs/.vuepress/config.js
================================================
import { defaultTheme } from '@vuepress/theme-default'
import { defineUserConfig } from 'vuepress'
import { viteBundler } from '@vuepress/bundler-vite'

export default defineUserConfig({
  lang: 'zh-CN',
  base: '/one-api-hub/',

  title: 'One API Hub - 中转站管理器',
  description: '一个开源的浏览器插件,聚合管理AI中转站账号的余额、模型和密钥,告别繁琐登录。',

  theme: defaultTheme({
    logo: 'https://github.com/fxaxg/one-api-hub/blob/main/assets/icon.png?raw=true',

    navbar: ['/', '/get-started', '/faq'],
  }),

  bundler: viteBundler(),
})


================================================
FILE: docs/docs/README.md
================================================
---
home: true
title: 首页
heroImage: https://github.com/fxaxg/one-api-hub/blob/main/assets/icon.png?raw=true
heroText: One API Hub - 中转站管理器
tagline: 一个开源的 AI 中转站账号管理插件
actions:
  - text: 开始使用
    link: /get-started.html # 建议修改为您的实际文档路径,例如 /guide/
    type: primary

  - text: 查看源码
    link: https://github.com/fxaxg/one-api-hub # 假设这是您的项目仓库地址
    type: secondary

  # ✨ 新增 Google Play 下载按钮
  - text: Chrome 应用商店
    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 
    type: secondary

features:
  - title: 站点自动识别
    details: 自动识别各类 AI 中转站点(如 One API, New API, Veloera),并自动创建系统访问 Token,添加到插件的站点列表中。
  - title: 充值比例识别
    details: 自动识别中转站的充值比例,让您清楚了解资金利用率。
  - title: 多账号支持
    details: 每个中转站点可添加和管理多个账号,满足您多账户需求。
  - title: 余额与日志查询
    details: 实时查看各账号的余额,并详细审查使用日志,掌握消费情况。
  - title: 令牌(Key)管理
    details: 便捷地查看与管理各站点的令牌(Key),确保安全与效率。
  - title: 模型与渠道信息
    details: 详细展示站点支持的模型信息和所关联的渠道,助您做出最佳选择。
  - title: 插件无需联网
    details: 核心功能无需联网即可使用,保护您的数据隐私和使用稳定性。

footer: MIT Licensed | Copyright © 2023-present Ai API Hub
---

## 介绍

目前市面上有太多 AI-API 中转站点,每次查看余额和支持模型列表等信息都非常麻烦,需要逐个登录查看。

本插件致力于解决这一痛点,可以便捷地对基于 
- [One-API]
- [New-API] 
- [Veloera](https://github.com/Veloera/Veloera)
等部署的 Ai 中转站账号进行整合管理,大大提升您的效率。

---

## 未来支持

-   **模型降智测试**: 探索并评估不同模型在特定任务上的表现。
-   **WebDAV 数据备份**: 支持将配置和数据备份到 WebDAV 服务,确保数据安全和可移植性。

[One-API]: https://github.com/songquanpeng/one-api
[New-API]: https://github.com/QuantumNous/new-api

================================================
FILE: docs/docs/faq.md
================================================
# 常见问题
收集一些插件使用时遇到的常见问题。
文档待完善,可先查看[使用教程](./get-started.md)

================================================
FILE: docs/docs/get-started.md
================================================
# 开始使用

一个开源的浏览器插件,聚合管理AI中转站账号的余额、模型和密钥,告别繁琐登录。

## 1. 下载

::: info 推荐
[前往 Chrome 应用商店]
:::

## 2. 支持的站点

支持基于以下项目部署的中转站:
 - [One-API] 
 - [New-API] 
 - [Veloera](https://github.com/Veloera/Veloera)

::: warning
如果站点进行了二次开发导致一些关键接口(例如`/api/user`)发生了改变,则插件可能无法正常添加此站点。
:::



## 3. 添加站点
::: info 提示
必须先使用浏览器,自行在目标中转站登录,这样插件的自动识别功能才能通过cookie获取到您账号的[访问令牌(Access_Token)](#_3-2-手动添加)
:::

### 3.1 自动识别添加

1. 打开插件主页面,点击`新增账号`

![新增账号](./static/image/add-account-btn.png)

2. 输入中转站地址,点击`自动识别`

![自动识别](./static/image/add-account-dialog-btn.png)

3. 确认自动识别无误后点击`确认添加`

:::info 提示
插件会自动识别您账号的:
- 用户名
- 用户ID
- [访问令牌(Access_Token)](#_3-2-手动添加)
- 充值金额比例
:::

![确认添加](./static/image/add-account-dialog-ok-btn.png)

### 3.2 手动添加

:::info 提示
当自动识别未成功后,可进行手动输入添加站点账号,需要先获取以下信息。(每个站点可能UI有所差异,请自行寻找)
:::
![用户信息](./static/image/site-user-info.png)

[One-API]: https://github.com/songquanpeng/one-api
[New-API]: https://github.com/QuantumNous/new-api
[前往 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


================================================
FILE: docs/package.json
================================================
{
  "name": "one-api-hub-docs",
  "description": "A VuePress project",
  "version": "0.0.1",
  "license": "MIT",
  "type": "module",
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "docs:build": "vuepress build docs",
    "docs:clean-dev": "vuepress dev docs --clean-cache",
    "docs:dev": "vuepress dev docs",
    "docs:update-package": "pnpm dlx vp-update"
  },
  "devDependencies": {
    "@vuepress/bundler-vite": "2.0.0-rc.20",
    "@vuepress/theme-default": "2.0.0-rc.88",
    "sass-embedded": "^1.86.0",
    "vue": "^3.5.13",
    "vuepress": "2.0.0-rc.20"
  }
}


================================================
FILE: examples/storageExample.ts
================================================
import { accountStorage, AccountStorageUtils } from "../services/accountStorage";

/**
 * 账号存储系统使用示例
 */
export class AccountStorageExample {
  /**
   * 添加新账号示例
   */
  static async addNewAccount() {
    try {
      const newAccountData = {
        emoji: "",
        site_name: "测试 API 站点",
        site_url: "https://api.test.com",
        health_status: "healthy" as const,
        exchange_rate: 7.2,
        account_info: {
          access_token: "sk-test-xxxxxxxxxxxxxxxxxxxx",
          username: "test_user@example.com",
          quota: 100.0,
          today_prompt_tokens: 0,
          today_completion_tokens: 0,
          today_quota_consumption: 0,
          today_requests_count: 0
        },
        last_sync_time: Date.now()
      };

      const accountId = await accountStorage.addAccount(newAccountData);
      console.log(`新账号已添加,ID: ${accountId}`);
      return accountId;
    } catch (error) {
      console.error('添加账号失败:', error);
      throw error;
    }
  }

  /**
   * 更新账号统计信息示例
   */
  static async updateAccountStats(accountId: string) {
    try {
      const updates = {
        account_info: {
          access_token: "保持原有token",
          username: "保持原有用户名",
          quota: 95.50, // 更新余额
          today_prompt_tokens: 1500,
          today_completion_tokens: 2300,
          today_quota_consumption: 4.50,
          today_requests_count: 15
        },
        last_sync_time: Date.now()
      };

      const success = await accountStorage.updateAccount(accountId, updates);
      console.log(`账号统计更新${success ? '成功' : '失败'}`);
      return success;
    } catch (error) {
      console.error('更新账号统计失败:', error);
      throw error;
    }
  }

  /**
   * 获取并显示所有账号信息示例
   */
  static async displayAllAccounts() {
    try {
      const accounts = await accountStorage.getAllAccounts();
      console.log(`当前共有 ${accounts.length} 个账号:`);
      
      accounts.forEach((account, index) => {
        console.log(`\n${index + 1}. ${account.site_name}`);
        console.log(`   用户名: ${account.account_info.username}`);
        console.log(`   余额: ${AccountStorageUtils.formatBalance(account.account_info.quota, 'USD')}`);
        console.log(`   今日消耗: ${AccountStorageUtils.formatBalance(account.account_info.today_quota_consumption, 'USD')}`);
        console.log(`   今日请求: ${account.account_info.today_requests_count} 次`);
        console.log(`   最后同步: ${new Date(account.last_sync_time).toLocaleString()}`);
      });

      return accounts;
    } catch (error) {
      console.error('获取账号信息失败:', error);
      throw error;
    }
  }

  /**
   * 获取统计信息示例
   */
  static async displayStats() {
    try {
      const stats = await accountStorage.getAccountStats();
      
      console.log('\n=== 总体统计 ===');
      console.log(`总余额: ${AccountStorageUtils.formatBalance(stats.total_quota, 'USD')}`);
      console.log(`今日总消耗: ${AccountStorageUtils.formatBalance(stats.today_total_consumption, 'USD')}`);
      console.log(`今日总请求: ${stats.today_total_requests} 次`);
      console.log(`今日 Prompt Tokens: ${AccountStorageUtils.formatTokenCount(stats.today_total_prompt_tokens)}`);
      console.log(`今日 Completion Tokens: ${AccountStorageUtils.formatTokenCount(stats.today_total_completion_tokens)}`);

      return stats;
    } catch (error) {
      console.error('获取统计信息失败:', error);
      throw error;
    }
  }

  /**
   * 数据转换示例(兼容现有 UI)
   */
  static async convertForUI() {
    try {
      const accounts = await accountStorage.getAllAccounts();
      const displayData = accountStorage.convertToDisplayData(accounts);
      
      console.log('\n=== 转换为 UI 数据格式 ===');
      console.log(JSON.stringify(displayData, null, 2));

      return displayData;
    } catch (error) {
      console.error('数据转换失败:', error);
      throw error;
    }
  }

  /**
   * 数据导出/导入示例
   */
  static async exportImportExample() {
    try {
      // 导出数据
      const exportedData = await accountStorage.exportData();
      console.log('数据已导出');
      
      // 模拟导入相同数据
      const importSuccess = await accountStorage.importData(exportedData);
      console.log(`数据导入${importSuccess ? '成功' : '失败'}`);

      return { exportedData, importSuccess };
    } catch (error) {
      console.error('导出/导入失败:', error);
      throw error;
    }
  }

  /**
   * 数据验证示例
   */
  static validateAccountData() {
    const validAccount = {
      emoji: "",
      site_name: "有效站点",
      site_url: "https://api.valid.com",
      health_status: "healthy" as const,
      exchange_rate: 7.2,
      account_info: {
        access_token: "sk-valid-token",
        username: "valid_user",
        quota: 100,
        today_prompt_tokens: 0,
        today_completion_tokens: 0,
        today_quota_consumption: 0,
        today_requests_count: 0
      }
    };

    const invalidAccount = {
      emoji: "",
      site_name: "", // 空的站点名称
      site_url: "invalid-url", // 不是有效URL
      health_status: undefined as any, // 缺少健康状态
      exchange_rate: -1, // 无效的充值比例
      account_info: {
        access_token: "", // 空的token
        username: "",
        quota: 100,
        today_prompt_tokens: 0,
        today_completion_tokens: 0,
        today_quota_consumption: 0,
        today_requests_count: 0
      }
    };

    console.log('\n=== 数据验证示例 ===');
    console.log('有效账号验证结果:', AccountStorageUtils.validateAccount(validAccount));
    console.log('无效账号验证结果:', AccountStorageUtils.validateAccount(invalidAccount));
  }

  /**
   * 运行所有示例
   */
  static async runAllExamples() {
    console.log('=== 账号存储系统示例 ===\n');
    
    try {
      // 数据验证示例
      this.validateAccountData();
      
      // 添加账号
      const accountId = await this.addNewAccount();
      
      // 显示所有账号
      await this.displayAllAccounts();
      
      // 更新账号统计
      await this.updateAccountStats(accountId);
      
      // 显示统计信息
      await this.displayStats();
      
      // 数据转换示例
      await this.convertForUI();
      
      // 导出/导入示例
      await this.exportImportExample();
      
      console.log('\n=== 所有示例运行完成 ===');
    } catch (error) {
      console.error('示例运行失败:', error);
    }
  }
}

// 如果直接运行此文件,执行所有示例
if (typeof window !== 'undefined') {
  // 浏览器环境
  (window as any).AccountStorageExample = AccountStorageExample;
}

================================================
FILE: global.d.ts
================================================
declare module "*.png" {
  const content: string;
  export default content;
}

declare module "*.jpg" {
  const content: string;
  export default content;
}

declare module "*.jpeg" {
  const content: string;
  export default content;
}

declare module "*.gif" {
  const content: string;
  export default content;
}

declare module "*.svg" {
  const content: string;
  export default content;
}

declare module "*.webp" {
  const content: string;
  export default content;
}

================================================
FILE: hooks/useAccountData.ts
================================================
import { useState, useEffect, useCallback } from "react"
import { accountStorage } from "../services/accountStorage"
import type { SiteAccount, AccountStats, DisplaySiteData } from "../types"

interface UseAccountDataResult {
  // 数据状态
  accounts: SiteAccount[]
  displayData: DisplaySiteData[]
  stats: AccountStats
  lastUpdateTime: Date
  
  // 加载状态
  isInitialLoad: boolean
  isRefreshing: boolean
  
  // 动画相关状态
  prevTotalConsumption: { USD: number; CNY: number }
  prevBalances: { [id: string]: { USD: number; CNY: number } }
  
  // 操作函数
  loadAccountData: () => Promise<void>
  handleRefresh: () => Promise<{ success: number; failed: number }>
}

export const useAccountData = (): UseAccountDataResult => {
  // 数据状态
  const [accounts, setAccounts] = useState<SiteAccount[]>([])
  const [displayData, setDisplayData] = useState<DisplaySiteData[]>([])
  const [stats, setStats] = useState<AccountStats>({
    total_quota: 0,
    today_total_consumption: 0,
    today_total_requests: 0,
    today_total_prompt_tokens: 0,
    today_total_completion_tokens: 0
  })
  const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date())
  
  // 加载状态
  const [isInitialLoad, setIsInitialLoad] = useState(true)
  const [isRefreshing, setIsRefreshing] = useState(false)
  
  // 动画相关状态
  const [prevTotalConsumption, setPrevTotalConsumption] = useState({ USD: 0, CNY: 0 })
  const [prevBalances, setPrevBalances] = useState<{ [id: string]: { USD: number, CNY: number } }>({})

  // 加载账号数据
  const loadAccountData = useCallback(async () => {
    try {
      const allAccounts = await accountStorage.getAllAccounts()
      const accountStats = await accountStorage.getAccountStats()
      const displaySiteData = accountStorage.convertToDisplayData(allAccounts)

      // 计算新的余额数据
      const newBalances: { [id: string]: { USD: number, CNY: number } } = {}
      displaySiteData.forEach(site => {
        newBalances[site.id] = {
          USD: site.balance.USD,
          CNY: site.balance.CNY
        }
      })

      // 如果不是初始加载,保存之前的数值供动画使用
      if (!isInitialLoad) {
        setPrevTotalConsumption(prevTotalConsumption)
        setPrevBalances(prevBalances)
      }

      // 更新状态
      setAccounts(allAccounts)
      setStats(accountStats)
      setDisplayData(displaySiteData)
      
      // 更新最后同步时间为最近的一次同步时间
      if (allAccounts.length > 0) {
        const latestSyncTime = Math.max(...allAccounts.map(acc => acc.last_sync_time))
        if (latestSyncTime > 0) {
          setLastUpdateTime(new Date(latestSyncTime))
        }
      }

      // 标记为非初始加载
      if (isInitialLoad) {
        setIsInitialLoad(false)
      }
      
      console.log('账号数据加载完成:', { 
        accountCount: allAccounts.length, 
        stats: accountStats 
      })
    } catch (error) {
      console.error('加载账号数据失败:', error)
    }
  }, [isInitialLoad, prevTotalConsumption, prevBalances])

  // 刷新数据
  const handleRefresh = useCallback(async () => {
    setIsRefreshing(true)
    try {
      // 刷新所有账号数据
      const refreshResult = await accountStorage.refreshAllAccounts()
      console.log('刷新结果:', refreshResult)
      
      // 重新加载显示数据
      await loadAccountData()
      setLastUpdateTime(new Date())
      
      // 返回刷新结果,让组件层处理 UI 反馈
      return refreshResult
    } catch (error) {
      console.error('刷新数据失败:', error)
      // 即使刷新失败也尝试加载本地数据
      await loadAccountData()
      throw error
    } finally {
      setIsRefreshing(false)
    }
  }, [loadAccountData])

  // 组件初始化时加载数据
  useEffect(() => {
    loadAccountData()
  }, [loadAccountData])

  return {
    // 数据状态
    accounts,
    displayData,
    stats,
    lastUpdateTime,
    
    // 加载状态
    isInitialLoad,
    isRefreshing,
    
    // 动画相关状态
    prevTotalConsumption,
    prevBalances,
    
    // 操作函数
    loadAccountData,
    handleRefresh
  }
}

================================================
FILE: hooks/useSort.ts
================================================
import { useState, useMemo, useCallback, useEffect } from "react"
import { UI_CONSTANTS } from "../constants/ui"
import { createSortComparator } from "../utils/formatters"
import type { DisplaySiteData } from "../types"

type SortField = 'name' | 'balance' | 'consumption'
type SortOrder = 'asc' | 'desc'

interface UseSortResult {
  sortField: SortField
  sortOrder: SortOrder
  sortedData: DisplaySiteData[]
  handleSort: (field: SortField) => void
}

export const useSort = (
  data: DisplaySiteData[],
  currencyType: 'USD' | 'CNY',
  initialSortField?: SortField,
  initialSortOrder?: SortOrder,
  onSortChange?: (field: SortField, order: SortOrder) => void
): UseSortResult => {
  const [sortField, setSortField] = useState<SortField>(
    initialSortField || UI_CONSTANTS.SORT.DEFAULT_FIELD
  )
  const [sortOrder, setSortOrder] = useState<SortOrder>(
    initialSortOrder || UI_CONSTANTS.SORT.DEFAULT_ORDER
  )

  // 当初始值变化时更新状态
  useEffect(() => {
    if (initialSortField !== undefined) {
      setSortField(initialSortField)
    }
  }, [initialSortField])

  useEffect(() => {
    if (initialSortOrder !== undefined) {
      setSortOrder(initialSortOrder)
    }
  }, [initialSortOrder])

  // 处理排序
  const handleSort = useCallback((field: SortField) => {
    let newOrder: SortOrder
    
    if (sortField === field) {
      newOrder = sortOrder === 'asc' ? 'desc' : 'asc'
      setSortOrder(newOrder)
    } else {
      newOrder = 'asc'
      setSortField(field)  
      setSortOrder(newOrder)
    }
    
    // 通知父组件排序变化
    onSortChange?.(field === sortField ? sortField : field, newOrder)
  }, [sortField, sortOrder, onSortChange])

  // 排序数据
  const sortedData = useMemo(() => {
    return [...data].sort((a, b) => {
      let aValue: string | number, bValue: string | number
      
      switch (sortField) {
        case 'name':
          aValue = a.name
          bValue = b.name
          break
        case 'balance':
          aValue = a.balance[currencyType]
          bValue = b.balance[currencyType]
          break
        case 'consumption':
          aValue = a.todayConsumption[currencyType]
          bValue = b.todayConsumption[currencyType]
          break
        default:
          return 0
      }
      
      if (sortOrder === 'asc') {
        return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
      } else {
        return aValue > bValue ? -1 : aValue < bValue ? 1 : 0
      }
    })
  }, [data, sortField, sortOrder, currencyType])

  return {
    sortField,
    sortOrder,
    sortedData,
    handleSort
  }
}

================================================
FILE: hooks/useTimeFormatter.ts
================================================
import { useState, useEffect } from "react"
import { UI_CONSTANTS } from "../constants/ui"
import { formatRelativeTime, formatFullTime } from "../utils/formatters"

interface UseTimeFormatterResult {
  formatRelativeTime: (date: Date) => string
  formatFullTime: (date: Date) => string
  forceUpdate: () => void
}

export const useTimeFormatter = (): UseTimeFormatterResult => {
  const [, setForceUpdate] = useState({})

  // 强制更新函数
  const forceUpdate = () => {
    setForceUpdate({})
  }

  // 定时更新相对时间显示
  useEffect(() => {
    const updateInterval = setInterval(() => {
      forceUpdate()
    }, UI_CONSTANTS.UPDATE_INTERVAL)

    return () => clearInterval(updateInterval)
  }, [])

  return {
    formatRelativeTime,
    formatFullTime,
    forceUpdate
  }
}

================================================
FILE: hooks/useUserPreferences.ts
================================================
import { useState, useEffect, useCallback } from 'react';
import { userPreferences, type UserPreferences } from '../services/userPreferences';

/**
 * 用户偏好设置管理Hook
 */
export function useUserPreferences() {
  const [preferences, setPreferences] = useState<UserPreferences | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // 加载偏好设置
  const loadPreferences = useCallback(async () => {
    try {
      setIsLoading(true);
      const prefs = await userPreferences.getPreferences();
      setPreferences(prefs);
      console.log('[useUserPreferences] 偏好设置加载成功:', prefs);
    } catch (error) {
      console.error('[useUserPreferences] 加载偏好设置失败:', error);
    } finally {
      setIsLoading(false);
    }
  }, []);

  // 初始化加载
  useEffect(() => {
    loadPreferences();
  }, [loadPreferences]);

  // 更新活动标签页
  const updateActiveTab = useCallback(async (activeTab: 'consumption' | 'balance') => {
    try {
      const success = await userPreferences.updateActiveTab(activeTab);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, activeTab } : null);
        console.log('[useUserPreferences] 活动标签页更新成功:', activeTab);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新活动标签页失败:', error);
      return false;
    }
  }, [preferences]);

  // 更新货币类型
  const updateCurrencyType = useCallback(async (currencyType: 'USD' | 'CNY') => {
    try {
      const success = await userPreferences.updateCurrencyType(currencyType);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, currencyType } : null);
        console.log('[useUserPreferences] 货币类型更新成功:', currencyType);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新货币类型失败:', error);
      return false;
    }
  }, [preferences]);

  // 更新排序配置
  const updateSortConfig = useCallback(async (sortField: 'name' | 'balance' | 'consumption', sortOrder: 'asc' | 'desc') => {
    try {
      const success = await userPreferences.updateSortConfig(sortField, sortOrder);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, sortField, sortOrder } : null);
        console.log('[useUserPreferences] 排序配置更新成功:', { sortField, sortOrder });
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新排序配置失败:', error);
      return false;
    }
  }, [preferences]);

  // 更新自动刷新设置
  const updateAutoRefresh = useCallback(async (autoRefresh: boolean) => {
    try {
      const success = await userPreferences.updateAutoRefresh(autoRefresh);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, autoRefresh } : null);
        console.log('[useUserPreferences] 自动刷新设置更新成功:', autoRefresh);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新自动刷新设置失败:', error);
      return false;
    }
  }, [preferences]);

  // 更新刷新间隔
  const updateRefreshInterval = useCallback(async (refreshInterval: number) => {
    try {
      const success = await userPreferences.updateRefreshInterval(refreshInterval);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, refreshInterval } : null);
        console.log('[useUserPreferences] 刷新间隔更新成功:', refreshInterval);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新刷新间隔失败:', error);
      return false;
    }
  }, [preferences]);

  // 更新打开插件时自动刷新设置
  const updateRefreshOnOpen = useCallback(async (refreshOnOpen: boolean) => {
    try {
      const success = await userPreferences.updateRefreshOnOpen(refreshOnOpen);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, refreshOnOpen } : null);
        console.log('[useUserPreferences] 打开插件时自动刷新设置更新成功:', refreshOnOpen);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新打开插件时自动刷新设置失败:', error);
      return false;
    }
  }, [preferences]);

  // 更新健康状态显示设置
  const updateShowHealthStatus = useCallback(async (showHealthStatus: boolean) => {
    try {
      const success = await userPreferences.updateShowHealthStatus(showHealthStatus);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, showHealthStatus } : null);
        console.log('[useUserPreferences] 健康状态显示设置更新成功:', showHealthStatus);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 更新健康状态显示设置失败:', error);
      return false;
    }
  }, [preferences]);

  // 批量更新偏好设置
  const updatePreferences = useCallback(async (updates: Partial<UserPreferences>) => {
    try {
      const success = await userPreferences.savePreferences(updates);
      if (success && preferences) {
        setPreferences(prev => prev ? { ...prev, ...updates } : null);
        console.log('[useUserPreferences] 偏好设置批量更新成功:', updates);
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 批量更新偏好设置失败:', error);
      return false;
    }
  }, [preferences]);

  // 重置为默认设置
  const resetToDefaults = useCallback(async () => {
    try {
      const success = await userPreferences.resetToDefaults();
      if (success) {
        await loadPreferences(); // 重新加载设置
        console.log('[useUserPreferences] 已重置为默认设置');
      }
      return success;
    } catch (error) {
      console.error('[useUserPreferences] 重置设置失败:', error);
      return false;
    }
  }, [loadPreferences]);

  return {
    // 状态
    preferences,
    isLoading,

    // 便捷访问属性
    activeTab: preferences?.activeTab || 'consumption',
    currencyType: preferences?.currencyType || 'USD',
    sortField: preferences?.sortField || 'name',
    sortOrder: preferences?.sortOrder || 'asc',
    autoRefresh: preferences?.autoRefresh ?? true,
    refreshInterval: preferences?.refreshInterval ?? 360,
    refreshOnOpen: preferences?.refreshOnOpen ?? true,
    showHealthStatus: preferences?.showHealthStatus ?? true,

    // 操作方法
    updateActiveTab,
    updateCurrencyType,
    updateSortConfig,
    updateAutoRefresh,
    updateRefreshInterval,
    updateRefreshOnOpen,
    updateShowHealthStatus,
    updatePreferences,
    resetToDefaults,
    loadPreferences
  };
}

================================================
FILE: options/index.tsx
================================================
import "../popup/style.css"
import { useState, useEffect } from "react"
import { 
  CogIcon,
  CpuChipIcon,
  KeyIcon,
  ArrowPathIcon,
  InformationCircleIcon
} from "@heroicons/react/24/outline"
import { Toaster } from 'react-hot-toast'
import iconImage from "../assets/icon.png"

// 页面组件导入
import BasicSettings from "./pages/BasicSettings"
import ModelList from "./pages/ModelList"
import KeyManagement from "./pages/KeyManagement"
import ImportExport from "./pages/ImportExport"
import About from "./pages/About"

// 菜单项类型定义
interface MenuItem {
  id: string
  name: string
  icon: React.ComponentType<{ className?: string }>
  component: React.ComponentType<any>
}

// 菜单配置
const menuItems: MenuItem[] = [
  {
    id: 'basic',
    name: '基本设置',
    icon: CogIcon,
    component: BasicSettings
  },
  {
    id: 'models',
    name: '模型列表',
    icon: CpuChipIcon,
    component: ModelList
  },
  {
    id: 'keys',
    name: '密钥管理',
    icon: KeyIcon,
    component: KeyManagement
  },
  {
    id: 'import-export',
    name: '导入/导出',
    icon: ArrowPathIcon,
    component: ImportExport
  },
  {
    id: 'about',
    name: '关于',
    icon: InformationCircleIcon,
    component: About
  }
]

// 解析URL hash和参数
function parseHash() {
  const hash = window.location.hash.slice(1) // 去掉 #
  if (!hash) return { page: 'basic', params: {} }
  
  const [page, ...paramParts] = hash.split('?')
  const params: Record<string, string> = {}
  
  if (paramParts.length > 0) {
    const paramString = paramParts.join('?')
    const urlParams = new URLSearchParams(paramString)
    for (const [key, value] of urlParams.entries()) {
      params[key] = value
    }
  }
  
  return { page: page || 'basic', params }
}

// 更新URL hash
function updateHash(page: string, params?: Record<string, string>) {
  let hash = `#${page}`
  if (params && Object.keys(params).length > 0) {
    const searchParams = new URLSearchParams(params)
    hash += `?${searchParams.toString()}`
  }
  window.history.replaceState(null, '', hash)
}

function OptionsPage() {
  const [activeMenuItem, setActiveMenuItem] = useState('basic')
  const [routeParams, setRouteParams] = useState<Record<string, string>>({})

  // 初始化路由
  useEffect(() => {
    const { page, params } = parseHash()
    const validPage = menuItems.find(item => item.id === page) ? page : 'basic'
    setActiveMenuItem(validPage)
    setRouteParams(params)
    
    // 监听浏览器前进后退
    const handleHashChange = () => {
      const { page, params } = parseHash()
      const validPage = menuItems.find(item => item.id === page) ? page : 'basic'
      setActiveMenuItem(validPage)
      setRouteParams(params)
    }
    
    window.addEventListener('hashchange', handleHashChange)
    return () => window.removeEventListener('hashchange', handleHashChange)
  }, [])

  // 切换菜单项
  const handleMenuItemChange = (itemId: string, params?: Record<string, string>) => {
    setActiveMenuItem(itemId)
    setRouteParams(params || {})
    updateHash(itemId, params)
  }

  // 获取当前活动的组件
  const ActiveComponent = menuItems.find(item => item.id === activeMenuItem)?.component || BasicSettings

  return (
    <div className="min-h-screen bg-gray-50">
      {/* 顶部导航栏 */}
      <header className="bg-white shadow-sm border-b border-gray-200">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex items-center h-16">
            {/* 插件图标和名称 */}
            <div className="flex items-center space-x-3">
              <img 
                src={iconImage} 
                alt="One API Hub" 
                className="w-8 h-8 rounded-lg shadow-sm"
              />
              <div>
                <h1 className="text-xl font-semibold text-gray-900">One API Hub</h1>
                <p className="text-sm text-gray-500">AI 中转站账号管理插件</p>
              </div>
            </div>
          </div>
        </div>
      </header>

      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        <div className="flex gap-8">
          {/* 左侧菜单导航栏 */}
          <aside className="w-64 flex-shrink-0">
            <nav className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
              <div className="p-4 border-b border-gray-100">
                <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide">设置选项</h2>
              </div>
              <ul className="divide-y divide-gray-100">
                {menuItems.map((item) => {
                  const Icon = item.icon
                  const isActive = activeMenuItem === item.id
                  
                  return (
                    <li key={item.id}>
                      <button
                        onClick={() => handleMenuItemChange(item.id)}
                        className={`w-full flex items-center px-4 py-3 text-left hover:bg-gray-50 transition-colors ${
                          isActive 
                            ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-600' 
                            : 'text-gray-700'
                        }`}
                      >
                        <Icon className={`w-5 h-5 mr-3 ${
                          isActive ? 'text-blue-600' : 'text-gray-400'
                        }`} />
                        <span className="font-medium">{item.name}</span>
                      </button>
                    </li>
                  )
                })}
              </ul>
            </nav>
          </aside>

          {/* 右侧内容区域 */}
          <main className="flex-1 min-w-0">
            <div className="bg-white rounded-lg shadow-sm border border-gray-200 min-h-[600px]">
              <ActiveComponent routeParams={routeParams} />
            </div>
          </main>
        </div>
      </div>
      <Toaster
        position="bottom-center"
        reverseOrder={false}
        gutter={8}
        containerClassName=""
        containerStyle={{}}
        toastOptions={{
          className: '',
          duration: 4000,
          style: {
            background: '#fff',
            color: '#363636',
          },
          success: {
            duration: 3000,
          },
          error: {
            duration: 5000,
          },
        }}
      />
    </div>
  )
}

export default OptionsPage

================================================
FILE: options/pages/About.tsx
================================================
import { InformationCircleIcon, HeartIcon, GlobeAltIcon, CodeBracketIcon } from "@heroicons/react/24/outline"
import iconImage from "../../assets/icon.png"
import packageJson from "../../package.json"

export default function About() {
  const version = packageJson.version

  const features = [
    "自动识别中转站点,自动创建系统访问 token",
    "每个站点可添加多个账号",
    "账号的余额、使用日志进行查看",
    "密钥(key)查看与管理",
    "站点支持模型信息和渠道查看",
    "插件无需联网,保护隐私安全"
  ]

  const futureFeatures = [
    "模型降智测试",
    "WebDAV 数据备份",
    "更多API站点支持",
    "高级统计分析功能"
  ]

  const techStack = [
    { name: "Plasmo", version: "0.90.5", description: "浏览器扩展开发框架" },
    { name: "React", version: "18.2.0", description: "用户界面库" },
    { name: "TypeScript", version: "5.3.3", description: "类型安全的JavaScript" },
    { name: "Tailwind CSS", version: "3.4.17", description: "原子化CSS框架" },
    { name: "Headless UI", version: "2.2.4", description: "无样式UI组件" }
  ]

  return (
    <div className="p-6">
      {/* 页面标题 */}
      <div className="mb-8">
        <div className="flex items-center space-x-3 mb-2">
          <InformationCircleIcon className="w-6 h-6 text-blue-600" />
          <h1 className="text-2xl font-semibold text-gray-900">关于</h1>
        </div>
        <p className="text-gray-500">了解插件信息和开发团队</p>
      </div>

      <div className="space-y-8">
        {/* 插件信息 */}
        <section>
          <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-6">
            <div className="flex items-start space-x-4">
              <img 
                src={iconImage} 
                alt="One API Hub" 
                className="w-16 h-16 rounded-lg shadow-sm flex-shrink-0"
              />
              <div className="flex-1">
                <h2 className="text-2xl font-bold text-gray-900 mb-2">One API Hub</h2>
                <p className="text-gray-600 mb-4">
                  AI 中转站账号管理插件,帮助用户便捷地管理多个AI API中转站点的账号。
                </p>
                <div className="text-sm">
                  <div>
                    <span className="text-gray-500">版本号:</span>
                    <span className="ml-2 font-medium text-gray-900">v{version}</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>

        {/* 项目链接 */}
        <section>
          <h2 className="text-lg font-medium text-gray-900 mb-4">项目链接</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
            <div className="bg-white border border-gray-200 rounded-lg p-6">
              <div className="flex items-start space-x-4">
                <CodeBracketIcon className="w-6 h-6 text-gray-900 mt-1 flex-shrink-0" />
                <div className="flex-1">
                  <h3 className="font-medium text-gray-900 mb-2">GitHub 仓库</h3>
                  <p className="text-sm text-gray-600 mb-3">
                    查看源代码、提交问题或参与项目开发
                  </p>
                  <a 
                    href="https://github.com/fxaxg/one-api-hub" 
                    target="_blank" 
                    rel="noopener noreferrer"
                    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"
                  >
                    去点个Star
                  </a>
                </div>
              </div>
            </div>
            <div className="bg-white border border-gray-200 rounded-lg p-6">
              <div className="flex items-start space-x-4">
                <GlobeAltIcon className="w-6 h-6 text-blue-600 mt-1 flex-shrink-0" />
                <div className="flex-1">
                  <h3 className="font-medium text-gray-900 mb-2">项目官网</h3>
                  <p className="text-sm text-gray-600 mb-3">
                    查看详细文档、使用指南和更多信息
                  </p>
                  <a 
                    href="https://fxaxg.github.io/one-api-hub/" 
                    target="_blank" 
                    rel="noopener noreferrer"
                    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"
                  >
                    访问官网
                  </a>
                </div>
              </div>
            </div>
          </div>
        </section>

        {/* 功能特性 */}
        <section>
          <h2 className="text-lg font-medium text-gray-900 mb-4">功能特性</h2>
          <div className="space-y-6">
            {/* 主要功能 */}
            <div>
              <h3 className="text-base font-medium text-gray-800 mb-3 flex items-center">
                <div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
                已实现功能
              </h3>
              <div className="bg-green-50 border border-green-200 rounded-lg p-4">
                <ul className="space-y-2">
                  {features.map((feature, index) => (
                    <li key={index} className="flex items-start space-x-2 text-sm text-green-800">
                      <div className="w-1.5 h-1.5 bg-green-500 rounded-full mt-2 flex-shrink-0"></div>
                      <span>{feature}</span>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
            
            {/* 未来功能 */}
            <div>
              <h3 className="text-base font-medium text-gray-800 mb-3 flex items-center">
                <div className="w-2 h-2 bg-blue-500 rounded-full mr-2"></div>
                即将支持
              </h3>
              <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
                <ul className="space-y-2">
                  {futureFeatures.map((feature, index) => (
                    <li key={index} className="flex items-start space-x-2 text-sm text-blue-800">
                      <div className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 flex-shrink-0"></div>
                      <span>{feature}</span>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          </div>
        </section>

        {/* 技术栈 */}
        <section>
          <h2 className="text-lg font-medium text-gray-900 mb-4">技术栈</h2>
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
            {techStack.map((tech, index) => (
              <div key={index} className="bg-white border border-gray-200 rounded-lg p-4">
                <div className="flex items-center justify-between mb-2">
                  <h3 className="font-medium text-gray-900">{tech.name}</h3>
                  <span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded">
                    v{tech.version}
                  </span>
                </div>
                <p className="text-sm text-gray-500">{tech.description}</p>
              </div>
            ))}
          </div>
        </section>


        {/* 版权和致谢 */}
        <section>
          <h2 className="text-lg font-medium text-gray-900 mb-4">版权与致谢</h2>
          <div className="bg-white border border-gray-200 rounded-lg p-6">
            <div className="flex items-start space-x-4">
              <HeartIcon className="w-6 h-6 text-red-500 mt-1 flex-shrink-0" />
              <div className="flex-1">
                <h3 className="font-medium text-gray-900 mb-2">开发与维护</h3>
                <p className="text-sm text-gray-600 mb-4">
                  感谢所有为开源社区做出贡献的开发者们,本插件的开发得益于这些优秀的开源项目和工具。
                </p>
                <div className="flex flex-wrap gap-2">
                  <span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
                    Made with ❤️
                  </span>
                  <span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
                    Open Source
                  </span>
                  <span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
                    Privacy First
                  </span>
                </div>
              </div>
            </div>
          </div>
        </section>

        {/* 隐私声明 */}
        <section>
          <div className="bg-green-50 border border-green-200 rounded-lg p-4">
            <div className="flex items-start space-x-3">
              <InformationCircleIcon className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
              <div className="text-sm">
                <p className="text-green-800 font-medium mb-1">隐私保护</p>
                <p className="text-green-700">
                  本插件所有数据均存储在本地浏览器中,不会上传到任何服务器。
                  您的账号信息和使用数据完全由您自己掌控,确保隐私安全。
                </p>
              </div>
            </div>
          </div>
        </section>
      </div>
    </div>
  )
}

================================================
FILE: options/pages/BasicSettings.tsx
================================================
import { useState, useEffect } from "react"
import { Switch } from "@headlessui/react"
import { CogIcon, GlobeAltIcon, EyeIcon, ArrowPathIcon } from "@heroicons/react/24/outline"
import { useUserPreferences } from "../../hooks/useUserPreferences"
import toast from 'react-hot-toast'

export default function BasicSettings() {
  const {
    preferences,
    isLoading,
    currencyType,
    activeTab,
    updateCurrencyType,
    updateActiveTab,
    updateAutoRefresh,
    updateRefreshInterval,
    updateRefreshOnOpen,
    resetToDefaults
  } = useUserPreferences()

  // 从偏好设置中获取值,或使用默认值
  const autoRefresh = preferences?.autoRefresh ?? true
  const refreshInterval = preferences?.refreshInterval ?? 360
  const refreshOnOpen = preferences?.refreshOnOpen ?? true

  // 本地状态用于输入框编辑
  const [intervalInput, setIntervalInput] = useState<string>(refreshInterval.toString())

  // 同步刷新间隔值到输入框
  useEffect(() => {
    setIntervalInput(refreshInterval.toString())
  }, [refreshInterval])

  const handleCurrencyChange = async (currency: 'USD' | 'CNY') => {
    const success = await updateCurrencyType(currency)
    if (success) {
      toast.success(`货币单位已切换到 ${currency === 'USD' ? '美元' : '人民币'}`)
    } else {
      toast.error('设置保存失败')
    }
  }

  const handleDefaultTabChange = async (tab: 'consumption' | 'balance') => {
    const success = await updateActiveTab(tab)
    if (success) {
      toast.success(`默认标签页已设置为 ${tab === 'consumption' ? '今日消耗' : '总余额'}`)
    } else {
      toast.error('设置保存失败')
    }
  }

  const handleAutoRefreshChange = async (enabled: boolean) => {
    const success = await updateAutoRefresh(enabled)
    if (success) {
      // 通知后台更新设置
      chrome.runtime.sendMessage({
        action: 'updateAutoRefreshSettings',
        settings: { autoRefresh: enabled }
      });
      toast.success(`自动刷新已${enabled ? '启用' : '关闭'}`)
    } else {
      toast.error('设置保存失败')
    }
  }

  const handleRefreshIntervalChange = async (value: string) => {
    // 直接更新输入框状态,允许用户清空和编辑
    setIntervalInput(value)
  }

  const handleRefreshIntervalBlur = async () => {
    const interval = Number(intervalInput)
    
    // 验证输入值
    if (!intervalInput || isNaN(interval) || interval < 10) {
      toast.error('刷新间隔必须大于等于10秒')
      setIntervalInput(refreshInterval.toString()) // 恢复原值
      return
    }

    // 保存设置
    const success = await updateRefreshInterval(interval)
    if (success) {
      // 通知后台更新设置
      chrome.runtime.sendMessage({
        action: 'updateAutoRefreshSettings',
        settings: { refreshInterval: interval }
      });
      toast.success(`刷新间隔已设置为 ${interval} 秒`)
    } else {
      toast.error('设置保存失败')
      setIntervalInput(refreshInterval.toString()) // 恢复原值
    }
  }

  const handleRefreshOnOpenChange = async (enabled: boolean) => {
    const success = await updateRefreshOnOpen(enabled)
    if (success) {
      toast.success(`打开插件时自动刷新已${enabled ? '启用' : '关闭'}`)
    } else {
      toast.error('设置保存失败')
    }
  }


  const handleResetToDefaults = async () => {
    if (window.confirm('确定要重置所有设置到默认值吗?此操作不可撤销。')) {
      const success = await resetToDefaults()
      if (success) {
        toast.success('所有设置已重置为默认值')
      } else {
        toast.error('重置失败')
      }
    }
  }

  if (isLoading) {
    return (
      <div className="p-6">
        <div className="animate-pulse">
          <div className="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
          <div className="space-y-3">
            <div className="h-16 bg-gray-200 rounded"></div>
            <div className="h-16 bg-gray-200 rounded"></div>
            <div className="h-16 bg-gray-200 rounded"></div>
          </div>
        </div>
      </div>
    )
  }

  return (
    <div className="p-6">
      {/* 页面标题 */}
      <div className="mb-8">
        <div className="flex items-center space-x-3 mb-2">
          <CogIcon className="w-6 h-6 text-blue-600" />
          <h1 className="text-2xl font-semibold text-gray-900">基本设置</h1>
        </div>
        <p className="text-gray-500">管理插件的基本配置选项</p>
      </div>

      <div 
Download .txt
gitextract_sbci5x21/

├── .github/
│   └── workflows/
│       ├── deploy-docs.yml
│       └── submit.yml
├── .gitignore
├── .prettierrc.mjs
├── CLAUDE.md
├── LICENSE
├── README.md
├── background.ts
├── components/
│   ├── AccountList.tsx
│   ├── ActionButtons.tsx
│   ├── AddAccountDialog.tsx
│   ├── AddTokenDialog.tsx
│   ├── AutoDetectErrorAlert.tsx
│   ├── BalanceSection.tsx
│   ├── CopyKeyDialog.tsx
│   ├── DelAccountDialog.tsx
│   ├── EditAccountDialog.tsx
│   ├── HeaderSection.tsx
│   ├── ModelItem.tsx
│   └── Tooltip.tsx
├── constants/
│   └── ui.ts
├── content.ts
├── debug/
│   └── testStorage.ts
├── docs/
│   ├── docs/
│   │   ├── .vuepress/
│   │   │   └── config.js
│   │   ├── README.md
│   │   ├── faq.md
│   │   └── get-started.md
│   └── package.json
├── examples/
│   └── storageExample.ts
├── global.d.ts
├── hooks/
│   ├── useAccountData.ts
│   ├── useSort.ts
│   ├── useTimeFormatter.ts
│   └── useUserPreferences.ts
├── options/
│   ├── index.tsx
│   └── pages/
│       ├── About.tsx
│       ├── BasicSettings.tsx
│       ├── ImportExport.tsx
│       ├── KeyManagement.tsx
│       └── ModelList.tsx
├── package.json
├── popup/
│   ├── index.tsx
│   └── style.css
├── postcss.config.js
├── services/
│   ├── accountOperations.ts
│   ├── accountStorage.ts
│   ├── apiService.ts
│   ├── autoRefreshService.ts
│   └── userPreferences.ts
├── tailwind.config.js
├── tsconfig.json
├── types/
│   └── index.ts
└── utils/
    ├── autoDetectUtils.ts
    ├── formatters.ts
    ├── modelPricing.ts
    └── modelProviders.ts
Download .txt
SYMBOL INDEX (170 symbols across 36 files)

FILE: background.ts
  function handleOpenTempWindow (line 55) | async function handleOpenTempWindow(request: any, sendResponse: Function) {
  function handleCloseTempWindow (line 81) | async function handleCloseTempWindow(request: any, sendResponse: Functio...
  function handleAutoDetectSite (line 98) | async function handleAutoDetectSite(request: any, sendResponse: Function) {
  function waitForTabComplete (line 164) | function waitForTabComplete(tabId: number): Promise<void> {

FILE: components/AccountList.tsx
  type SortField (line 12) | type SortField = 'name' | 'balance' | 'consumption'
  type SortOrder (line 13) | type SortOrder = 'asc' | 'desc'
  type AccountListProps (line 15) | interface AccountListProps {
  function AccountList (line 43) | function AccountList({

FILE: components/ActionButtons.tsx
  type ActionButtonsProps (line 5) | interface ActionButtonsProps {
  function ActionButtons (line 11) | function ActionButtons({ onAddAccount, onViewKeys, onViewModels }: Actio...

FILE: components/AddAccountDialog.tsx
  type AddAccountDialogProps (line 9) | interface AddAccountDialogProps {
  function AddAccountDialog (line 14) | function AddAccountDialog({ isOpen, onClose }: AddAccountDialogProps) {

FILE: components/AddTokenDialog.tsx
  type AddTokenDialogProps (line 21) | interface AddTokenDialogProps {
  type FormData (line 35) | interface FormData {
  function AddTokenDialog (line 47) | function AddTokenDialog({ isOpen, onClose, availableAccounts, preSelecte...

FILE: components/AutoDetectErrorAlert.tsx
  function AutoDetectErrorAlert (line 6) | function AutoDetectErrorAlert({

FILE: components/BalanceSection.tsx
  type BalanceSectionProps (line 9) | interface BalanceSectionProps {
  function BalanceSection (line 29) | function BalanceSection({

FILE: components/CopyKeyDialog.tsx
  type CopyKeyDialogProps (line 19) | interface CopyKeyDialogProps {
  function CopyKeyDialog (line 25) | function CopyKeyDialog({ isOpen, onClose, account }: CopyKeyDialogProps) {

FILE: components/DelAccountDialog.tsx
  type DelAccountDialogProps (line 8) | interface DelAccountDialogProps {
  function DelAccountDialog (line 15) | function DelAccountDialog({ isOpen, onClose, account, onDeleted }: DelAc...

FILE: components/EditAccountDialog.tsx
  type EditAccountDialogProps (line 11) | interface EditAccountDialogProps {
  function EditAccountDialog (line 17) | function EditAccountDialog({ isOpen, onClose, account }: EditAccountDial...

FILE: components/HeaderSection.tsx
  type HeaderSectionProps (line 6) | interface HeaderSectionProps {
  function HeaderSection (line 13) | function HeaderSection({

FILE: components/ModelItem.tsx
  type ModelItemProps (line 29) | interface ModelItemProps {
  function ModelItem (line 42) | function ModelItem({

FILE: components/Tooltip.tsx
  type TooltipProps (line 4) | interface TooltipProps {
  function Tooltip (line 12) | function Tooltip({

FILE: constants/ui.ts
  constant UI_CONSTANTS (line 5) | const UI_CONSTANTS = {
  constant CURRENCY_SYMBOLS (line 76) | const CURRENCY_SYMBOLS = {
  constant HEALTH_STATUS_MAP (line 81) | const HEALTH_STATUS_MAP = {

FILE: content.ts
  function waitForUserInfo (line 79) | async function waitForUserInfo(

FILE: examples/storageExample.ts
  class AccountStorageExample (line 6) | class AccountStorageExample {
    method addNewAccount (line 10) | static async addNewAccount() {
    method updateAccountStats (line 42) | static async updateAccountStats(accountId: string) {
    method displayAllAccounts (line 69) | static async displayAllAccounts() {
    method displayStats (line 93) | static async displayStats() {
    method convertForUI (line 114) | static async convertForUI() {
    method exportImportExample (line 132) | static async exportImportExample() {
    method validateAccountData (line 152) | static validateAccountData() {
    method runAllExamples (line 195) | static async runAllExamples() {

FILE: hooks/useAccountData.ts
  type UseAccountDataResult (line 5) | interface UseAccountDataResult {

FILE: hooks/useSort.ts
  type SortField (line 6) | type SortField = 'name' | 'balance' | 'consumption'
  type SortOrder (line 7) | type SortOrder = 'asc' | 'desc'
  type UseSortResult (line 9) | interface UseSortResult {

FILE: hooks/useTimeFormatter.ts
  type UseTimeFormatterResult (line 5) | interface UseTimeFormatterResult {

FILE: hooks/useUserPreferences.ts
  function useUserPreferences (line 7) | function useUserPreferences() {

FILE: options/index.tsx
  type MenuItem (line 21) | interface MenuItem {
  function parseHash (line 63) | function parseHash() {
  function updateHash (line 82) | function updateHash(page: string, params?: Record<string, string>) {
  function OptionsPage (line 91) | function OptionsPage() {

FILE: options/pages/About.tsx
  function About (line 5) | function About() {

FILE: options/pages/BasicSettings.tsx
  function BasicSettings (line 7) | function BasicSettings() {

FILE: options/pages/ImportExport.tsx
  function ImportExport (line 14) | function ImportExport() {

FILE: options/pages/KeyManagement.tsx
  function KeyManagement (line 18) | function KeyManagement({ routeParams }: { routeParams?: Record<string, s...

FILE: options/pages/ModelList.tsx
  function ModelList (line 31) | function ModelList({ routeParams }: { routeParams?: Record<string, strin...

FILE: popup/index.tsx
  function IndexPopup (line 18) | function IndexPopup() {

FILE: services/accountOperations.ts
  type AccountValidationResult (line 34) | interface AccountValidationResult {
  type AccountSaveResult (line 47) | interface AccountSaveResult {
  function autoDetectAccount (line 54) | async function autoDetectAccount(url: string): Promise<AccountValidation...
  function validateAndSaveAccount (line 131) | async function validateAndSaveAccount(
  function validateAndUpdateAccount (line 189) | async function validateAndUpdateAccount(
  function extractDomainPrefix (line 251) | function extractDomainPrefix(hostname: string): string {
  function isValidExchangeRate (line 279) | function isValidExchangeRate(rate: string): boolean {

FILE: services/accountStorage.ts
  constant STORAGE_KEYS (line 13) | const STORAGE_KEYS = {
  constant DEFAULT_CONFIG (line 19) | const DEFAULT_CONFIG: StorageConfig = {
  class AccountStorageService (line 24) | class AccountStorageService {
    method constructor (line 27) | constructor() {
    method getAllAccounts (line 36) | async getAllAccounts(): Promise<SiteAccount[]> {
    method getAccountById (line 49) | async getAccountById(id: string): Promise<SiteAccount | null> {
    method addAccount (line 62) | async addAccount(accountData: Omit<SiteAccount, 'id' | 'created_at' | ...
    method updateAccount (line 91) | async updateAccount(id: string, updates: Partial<Omit<SiteAccount, 'id...
    method deleteAccount (line 117) | async deleteAccount(id: string): Promise<boolean> {
    method updateSyncTime (line 138) | async updateSyncTime(id: string): Promise<boolean> {
    method refreshAccount (line 148) | async refreshAccount(id: string): Promise<boolean> {
    method refreshAllAccounts (line 208) | async refreshAllAccounts(): Promise<{ success: number; failed: number ...
    method getAccountStats (line 234) | async getAccountStats(): Promise<AccountStats> {
    method convertToDisplayData (line 266) | convertToDisplayData(accounts: SiteAccount[]): DisplaySiteData[] {
    method clearAllData (line 294) | async clearAllData(): Promise<boolean> {
    method exportData (line 308) | async exportData(): Promise<StorageConfig> {
    method importData (line 315) | async importData(data: StorageConfig): Promise<boolean> {
    method getStorageConfig (line 333) | private async getStorageConfig(): Promise<StorageConfig> {
    method saveAccounts (line 346) | private async saveAccounts(accounts: SiteAccount[]): Promise<void> {
    method generateId (line 366) | private generateId(): string {
  method formatBalance (line 379) | formatBalance(amount: number, currency: CurrencyType): string {
  method formatTokenCount (line 387) | formatTokenCount(count: number): string {
  method validateAccount (line 399) | validateAccount(account: Partial<SiteAccount>): string[] {
  method getRandomEmoji (line 432) | getRandomEmoji(): string {
  method getHealthStatusInfo (line 439) | getHealthStatusInfo(status: SiteHealthStatus): { text: string; color: st...
  method isAccountStale (line 456) | isAccountStale(account: SiteAccount, maxAgeMinutes: number = 30): boolean {
  method getStaleAccounts (line 465) | getStaleAccounts(accounts: SiteAccount[], maxAgeMinutes: number = 30): S...
  method validateAccounts (line 472) | async validateAccounts(accounts: SiteAccount[]): Promise<{ valid: SiteAc...

FILE: services/apiService.ts
  type UserInfo (line 6) | interface UserInfo {
  type AccessTokenInfo (line 12) | interface AccessTokenInfo {
  type TodayUsageData (line 17) | interface TodayUsageData {
  type AccountData (line 24) | interface AccountData extends TodayUsageData {
  type RefreshAccountResult (line 28) | interface RefreshAccountResult {
  type HealthCheckResult (line 34) | interface HealthCheckResult {
  type SiteStatusInfo (line 39) | interface SiteStatusInfo {
  type ModelsResponse (line 46) | interface ModelsResponse {
  type GroupInfo (line 53) | interface GroupInfo {
  type GroupsResponse (line 59) | interface GroupsResponse {
  type CreateTokenRequest (line 66) | interface CreateTokenRequest {
  type CreateTokenResponse (line 78) | interface CreateTokenResponse {
  type ApiToken (line 84) | interface ApiToken {
  type ModelPricing (line 105) | interface ModelPricing {
  type PricingResponse (line 118) | interface PricingResponse {
  type PaginatedTokenResponse (line 126) | interface PaginatedTokenResponse {
  type ApiResponse (line 134) | interface ApiResponse<T = any> {
  type LogItem (line 141) | interface LogItem {
  type LogResponseData (line 148) | interface LogResponseData {
  constant REQUEST_CONFIG (line 154) | const REQUEST_CONFIG = {
  class ApiError (line 164) | class ApiError extends Error {
    method constructor (line 165) | constructor(

FILE: services/autoRefreshService.ts
  class AutoRefreshService (line 8) | class AutoRefreshService {
    method initialize (line 15) | async initialize() {
    method setupAutoRefresh (line 33) | async setupAutoRefresh() {
    method performBackgroundRefresh (line 65) | private async performBackgroundRefresh() {
    method refreshNow (line 84) | async refreshNow(): Promise<{ success: number; failed: number }> {
    method stopAutoRefresh (line 99) | stopAutoRefresh() {
    method updateSettings (line 110) | async updateSettings(settings: {
    method getStatus (line 126) | getStatus() {
    method notifyFrontend (line 136) | private notifyFrontend(type: string, data: any) {
    method destroy (line 159) | destroy() {

FILE: services/userPreferences.ts
  type UserPreferences (line 4) | interface UserPreferences {
  constant STORAGE_KEYS (line 24) | const STORAGE_KEYS = {
  constant DEFAULT_PREFERENCES (line 29) | const DEFAULT_PREFERENCES: UserPreferences = {
  class UserPreferencesService (line 41) | class UserPreferencesService {
    method constructor (line 44) | constructor() {
    method getPreferences (line 53) | async getPreferences(): Promise<UserPreferences> {
    method savePreferences (line 66) | async savePreferences(preferences: Partial<UserPreferences>): Promise<...
    method updateActiveTab (line 87) | async updateActiveTab(activeTab: 'consumption' | 'balance'): Promise<b...
    method updateCurrencyType (line 94) | async updateCurrencyType(currencyType: 'USD' | 'CNY'): Promise<boolean> {
    method updateSortConfig (line 101) | async updateSortConfig(sortField: 'name' | 'balance' | 'consumption', ...
    method updateAutoRefreshSettings (line 108) | async updateAutoRefreshSettings(settings: {
    method updateAutoRefresh (line 120) | async updateAutoRefresh(autoRefresh: boolean): Promise<boolean> {
    method updateRefreshInterval (line 127) | async updateRefreshInterval(refreshInterval: number): Promise<boolean> {
    method updateRefreshOnOpen (line 134) | async updateRefreshOnOpen(refreshOnOpen: boolean): Promise<boolean> {
    method updateShowHealthStatus (line 141) | async updateShowHealthStatus(showHealthStatus: boolean): Promise<boole...
    method resetToDefaults (line 148) | async resetToDefaults(): Promise<boolean> {
    method clearPreferences (line 162) | async clearPreferences(): Promise<boolean> {
    method exportPreferences (line 176) | async exportPreferences(): Promise<UserPreferences> {
    method importPreferences (line 183) | async importPreferences(preferences: UserPreferences): Promise<boolean> {
  method validatePreferences (line 206) | validatePreferences(preferences: Partial<UserPreferences>): string[] {
  method getTabDisplayName (line 249) | getTabDisplayName(tab: 'consumption' | 'balance'): string {
  method getCurrencySymbol (line 256) | getCurrencySymbol(currency: 'USD' | 'CNY'): string {
  method getSortFieldDisplayName (line 263) | getSortFieldDisplayName(field: 'name' | 'balance' | 'consumption'): stri...
  method getSortOrderDisplayName (line 275) | getSortOrderDisplayName(order: 'asc' | 'desc'): string {

FILE: types/index.ts
  type SiteHealthStatus (line 4) | type SiteHealthStatus = 'healthy' | 'warning' | 'error' | 'unknown';
  type AccountInfo (line 7) | interface AccountInfo {
  type SiteAccount (line 19) | interface SiteAccount {
  type StorageConfig (line 33) | interface StorageConfig {
  type AccountStats (line 39) | interface AccountStats {
  type ApiResponse (line 48) | interface ApiResponse<T = any> {
  type SortField (line 55) | type SortField = 'site_name' | 'quota' | 'today_quota_consumption' | 'la...
  type SortOrder (line 56) | type SortOrder = 'asc' | 'desc';
  type CurrencyType (line 59) | type CurrencyType = 'USD' | 'CNY';
  type DisplaySiteData (line 62) | interface DisplaySiteData {

FILE: utils/autoDetectUtils.ts
  type AutoDetectErrorType (line 20) | enum AutoDetectErrorType {
  type AutoDetectError (line 29) | interface AutoDetectError {
  function analyzeAutoDetectError (line 38) | function analyzeAutoDetectError(error: string | Error): AutoDetectError {
  type AutoDetectErrorProps (line 88) | interface AutoDetectErrorProps {
  function getLoginUrl (line 96) | function getLoginUrl(siteUrl: string): string {
  function openLoginTab (line 107) | function openLoginTab(siteUrl: string): void {

FILE: utils/modelPricing.ts
  type CalculatedPrice (line 7) | interface CalculatedPrice {

FILE: utils/modelProviders.ts
  type ProviderType (line 8) | type ProviderType = 'OpenAI' | 'Claude' | 'Gemini' | 'Grok' | 'Qwen' | '...
  type ProviderConfig (line 11) | interface ProviderConfig {
  constant PROVIDER_CONFIGS (line 20) | const PROVIDER_CONFIGS: Record<ProviderType, ProviderConfig> = {
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (380K chars).
[
  {
    "path": ".github/workflows/deploy-docs.yml",
    "chars": 1715,
    "preview": "name: 部署文档\n\non:\n  push:\n    branches:\n      # 确保这是你正在使用的分支名称\n      - dev\n    paths: # <<<<<< 新增:路径过滤\n      - 'docs/docs/"
  },
  {
    "path": ".github/workflows/submit.yml",
    "chars": 932,
    "preview": "name: \"Submit to Web Store\"\non:\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses:"
  },
  {
    "path": ".gitignore",
    "chars": 531,
    "preview": "\n# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.p"
  },
  {
    "path": ".prettierrc.mjs",
    "chars": 558,
    "preview": "/**\n * @type {import('prettier').Options}\n */\nexport default {\n  printWidth: 80,\n  tabWidth: 2,\n  useTabs: false,\n  semi"
  },
  {
    "path": "CLAUDE.md",
    "chars": 464,
    "preview": "## 用户定义\n- 当前项目使用 Plasmo v0.90.5 开发\n- 前端技术栈(Tailwind CSS v3、Headless UI等)\n- 当技术文档不确定时,应当使用 mcp 工具 context7 进行搜索\n\n### 项目介绍"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2025 fxaxg\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README.md",
    "chars": 2945,
    "preview": "<div align=\"center\">\n  <img src=\"assets/icon.png\" alt=\"One API Hub Logo\" width=\"128\" height=\"128\">\n  \n  # 中转站管理器 - One A"
  },
  {
    "path": "background.ts",
    "chars": 4683,
    "preview": "import {\n  autoRefreshService,\n  handleAutoRefreshMessage\n} from \"./services/autoRefreshService\"\n\n// 管理临时窗口的 Map\nconst t"
  },
  {
    "path": "components/AccountList.tsx",
    "chars": 13589,
    "preview": "import { ChevronUpIcon, ChevronDownIcon, ChartBarIcon, CpuChipIcon, EllipsisHorizontalIcon, DocumentDuplicateIcon, Chart"
  },
  {
    "path": "components/ActionButtons.tsx",
    "chars": 1193,
    "preview": "import { PlusIcon, KeyIcon, CpuChipIcon } from \"@heroicons/react/24/outline\"\nimport { UI_CONSTANTS } from \"../constants/"
  },
  {
    "path": "components/AddAccountDialog.tsx",
    "chars": 25328,
    "preview": "import { useState, useEffect, Fragment } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, "
  },
  {
    "path": "components/AddTokenDialog.tsx",
    "chars": 23068,
    "preview": "import { useState, useEffect, Fragment } from 'react'\nimport { Dialog, Transition, Switch } from '@headlessui/react'\nimp"
  },
  {
    "path": "components/AutoDetectErrorAlert.tsx",
    "chars": 2289,
    "preview": "import { Fragment } from 'react'\nimport { ExclamationTriangleIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/ou"
  },
  {
    "path": "components/BalanceSection.tsx",
    "chars": 5789,
    "preview": "import { Tab, TabGroup, TabList, TabPanel, TabPanels } from \"@headlessui/react\"\nimport { ArrowUpIcon, ArrowDownIcon } fr"
  },
  {
    "path": "components/CopyKeyDialog.tsx",
    "chars": 16064,
    "preview": "import { Fragment, useState, useEffect } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, "
  },
  {
    "path": "components/DelAccountDialog.tsx",
    "chars": 6441,
    "preview": "import { Fragment } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, DialogTitle, Transiti"
  },
  {
    "path": "components/EditAccountDialog.tsx",
    "chars": 21399,
    "preview": "import { useState, useEffect, Fragment } from \"react\"\nimport toast from 'react-hot-toast'\nimport { Dialog, DialogPanel, "
  },
  {
    "path": "components/HeaderSection.tsx",
    "chars": 1849,
    "preview": "import { ArrowsPointingOutIcon, Cog6ToothIcon, ArrowPathIcon } from \"@heroicons/react/24/outline\"\nimport { UI_CONSTANTS "
  },
  {
    "path": "components/ModelItem.tsx",
    "chars": 11341,
    "preview": "/**\n * 模型列表项组件\n */\n\nimport React, { useState } from 'react'\nimport { \n  DocumentDuplicateIcon, \n  ChevronDownIcon, \n  Ch"
  },
  {
    "path": "components/Tooltip.tsx",
    "chars": 4201,
    "preview": "import { useState, useRef, useEffect } from \"react\"\nimport type { ReactNode } from \"react\"\n\ninterface TooltipProps {\n  c"
  },
  {
    "path": "constants/ui.ts",
    "chars": 2207,
    "preview": "/**\n * UI 相关常量定义\n */\n\nexport const UI_CONSTANTS = {\n  // 弹窗尺寸\n  POPUP: {\n    WIDTH: 'w-96',\n    HEIGHT: 'h-[600px]',\n   "
  },
  {
    "path": "content.ts",
    "chars": 2537,
    "preview": "import type { PlasmoCSConfig } from \"plasmo\"\n\nimport { fetchUserInfo } from \"~services/apiService\"\n\nexport const config:"
  },
  {
    "path": "debug/testStorage.ts",
    "chars": 1943,
    "preview": "import { accountStorage } from \"../services/accountStorage\";\n\n/**\n * 存储功能测试脚本\n */\nexport const testStorageFunction = asy"
  },
  {
    "path": "docs/docs/.vuepress/config.js",
    "chars": 511,
    "preview": "import { defaultTheme } from '@vuepress/theme-default'\nimport { defineUserConfig } from 'vuepress'\nimport { viteBundler "
  },
  {
    "path": "docs/docs/README.md",
    "chars": 1519,
    "preview": "---\nhome: true\ntitle: 首页\nheroImage: https://github.com/fxaxg/one-api-hub/blob/main/assets/icon.png?raw=true\nheroText: On"
  },
  {
    "path": "docs/docs/faq.md",
    "chars": 59,
    "preview": "# 常见问题\n收集一些插件使用时遇到的常见问题。\n文档待完善,可先查看[使用教程](./get-started.md)"
  },
  {
    "path": "docs/docs/get-started.md",
    "chars": 1090,
    "preview": "# 开始使用\n\n一个开源的浏览器插件,聚合管理AI中转站账号的余额、模型和密钥,告别繁琐登录。\n\n## 1. 下载\n\n::: info 推荐\n[前往 Chrome 应用商店]\n:::\n\n## 2. 支持的站点\n\n支持基于以下项目部署的中转站"
  },
  {
    "path": "docs/package.json",
    "chars": 574,
    "preview": "{\n  \"name\": \"one-api-hub-docs\",\n  \"description\": \"A VuePress project\",\n  \"version\": \"0.0.1\",\n  \"license\": \"MIT\",\n  \"type"
  },
  {
    "path": "examples/storageExample.ts",
    "chars": 6224,
    "preview": "import { accountStorage, AccountStorageUtils } from \"../services/accountStorage\";\n\n/**\n * 账号存储系统使用示例\n */\nexport class Ac"
  },
  {
    "path": "global.d.ts",
    "chars": 474,
    "preview": "declare module \"*.png\" {\n  const content: string;\n  export default content;\n}\n\ndeclare module \"*.jpg\" {\n  const content:"
  },
  {
    "path": "hooks/useAccountData.ts",
    "chars": 3822,
    "preview": "import { useState, useEffect, useCallback } from \"react\"\nimport { accountStorage } from \"../services/accountStorage\"\nimp"
  },
  {
    "path": "hooks/useSort.ts",
    "chars": 2554,
    "preview": "import { useState, useMemo, useCallback, useEffect } from \"react\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport "
  },
  {
    "path": "hooks/useTimeFormatter.ts",
    "chars": 766,
    "preview": "import { useState, useEffect } from \"react\"\nimport { UI_CONSTANTS } from \"../constants/ui\"\nimport { formatRelativeTime, "
  },
  {
    "path": "hooks/useUserPreferences.ts",
    "chars": 6280,
    "preview": "import { useState, useEffect, useCallback } from 'react';\nimport { userPreferences, type UserPreferences } from '../serv"
  },
  {
    "path": "options/index.tsx",
    "chars": 6260,
    "preview": "import \"../popup/style.css\"\nimport { useState, useEffect } from \"react\"\nimport { \n  CogIcon,\n  CpuChipIcon,\n  KeyIcon,\n "
  },
  {
    "path": "options/pages/About.tsx",
    "chars": 8904,
    "preview": "import { InformationCircleIcon, HeartIcon, GlobeAltIcon, CodeBracketIcon } from \"@heroicons/react/24/outline\"\nimport ico"
  },
  {
    "path": "options/pages/BasicSettings.tsx",
    "chars": 11458,
    "preview": "import { useState, useEffect } from \"react\"\nimport { Switch } from \"@headlessui/react\"\nimport { CogIcon, GlobeAltIcon, E"
  },
  {
    "path": "options/pages/ImportExport.tsx",
    "chars": 14234,
    "preview": "import { useState } from \"react\"\nimport { \n  ArrowPathIcon,\n  ArrowUpTrayIcon,\n  ArrowDownTrayIcon,\n  DocumentIcon,\n  Ex"
  },
  {
    "path": "options/pages/KeyManagement.tsx",
    "chars": 15937,
    "preview": "import { useState, useEffect } from \"react\"\nimport { \n  KeyIcon, \n  MagnifyingGlassIcon, \n  PlusIcon,\n  DocumentDuplicat"
  },
  {
    "path": "options/pages/ModelList.tsx",
    "chars": 24863,
    "preview": "import { useState, useEffect, useMemo, useRef } from \"react\"\nimport { \n  CpuChipIcon, \n  MagnifyingGlassIcon, \n  Adjustm"
  },
  {
    "path": "package.json",
    "chars": 1293,
    "preview": "{\n  \"name\": \"one-api-hub\",\n  \"displayName\": \"中转站管理器 - One API Hub\",\n  \"version\": \"0.0.3\",\n  \"description\": \"一站式聚合管理所有AI中"
  },
  {
    "path": "popup/index.tsx",
    "chars": 9996,
    "preview": "import \"./style.css\"\nimport { useState, useCallback, useMemo, useEffect } from \"react\"\nimport toast, { Toaster } from 'r"
  },
  {
    "path": "popup/style.css",
    "chars": 58,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;"
  },
  {
    "path": "postcss.config.js",
    "chars": 81,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}"
  },
  {
    "path": "services/accountOperations.ts",
    "chars": 8081,
    "preview": "/**\n * 账号操作服务模块\n * \n * 作用:\n * 1. 提供账号自动识别功能,通过 Chrome 扩展 API 获取站点用户信息\n * 2. 处理账号的验证、保存和更新操作\n * 3. 封装账号数据的存储逻辑,包括余额获取和数据同"
  },
  {
    "path": "services/accountStorage.ts",
    "chars": 13311,
    "preview": "import { Storage } from \"@plasmohq/storage\";\nimport { refreshAccountData } from './apiService';\nimport type { \n  SiteAcc"
  },
  {
    "path": "services/apiService.ts",
    "chars": 18608,
    "preview": "/**\n * API 服务 - 用于与 One API/New API 站点进行交互\n */\n\n// ============= 类型定义 =============\nexport interface UserInfo {\n  id: nu"
  },
  {
    "path": "services/autoRefreshService.ts",
    "chars": 5106,
    "preview": "import { userPreferences } from './userPreferences';\nimport { accountStorage } from './accountStorage';\n\n/**\n * 自动刷新服务\n "
  },
  {
    "path": "services/userPreferences.ts",
    "chars": 7158,
    "preview": "import { Storage } from \"@plasmohq/storage\";\n\n// 用户偏好设置类型定义\nexport interface UserPreferences {\n  // BalanceSection 相关配置\n"
  },
  {
    "path": "tailwind.config.js",
    "chars": 634,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"!./node_modules\", \"./**/*.{js,ts,jsx,tsx}\"],"
  },
  {
    "path": "tsconfig.json",
    "chars": 295,
    "preview": "{\n  \"extends\": \"plasmo/templates/tsconfig.base\",\n  \"exclude\": [\n    \"node_modules\"\n  ],\n  \"include\": [\n    \".plasmo/inde"
  },
  {
    "path": "types/index.ts",
    "chars": 1913,
    "preview": "// 账号信息数据类型定义\n\n// 站点健康状态\nexport type SiteHealthStatus = 'healthy' | 'warning' | 'error' | 'unknown';\n\n// 账号基础信息\nexport i"
  },
  {
    "path": "utils/autoDetectUtils.ts",
    "chars": 2886,
    "preview": "/**\n * 自动识别错误处理工具模块\n * \n * 作用:\n * 1. 定义自动识别过程中可能出现的错误类型枚举\n * 2. 提供智能错误分析功能,将通用错误信息转换为结构化的错误对象\n * 3. 包含错误处理的辅助函数,如打开登录页面等"
  },
  {
    "path": "utils/formatters.ts",
    "chars": 3544,
    "preview": "import dayjs from \"dayjs\"\nimport relativeTime from \"dayjs/plugin/relativeTime\"\nimport \"dayjs/locale/zh-cn\"\nimport { UI_C"
  },
  {
    "path": "utils/modelPricing.ts",
    "chars": 3412,
    "preview": "/**\n * 模型定价计算工具\n */\n\nimport type { ModelPricing } from '../services/apiService'\n\nexport interface CalculatedPrice {\n  in"
  },
  {
    "path": "utils/modelProviders.ts",
    "chars": 2620,
    "preview": "/**\n * 模型厂商识别和图标映射工具\n */\n\nimport { OpenAI, Claude, Gemini, Grok, Qwen, DeepSeek } from '@lobehub/icons'\n\n// 厂商类型\nexport "
  }
]

About this extraction

This page contains the full source code of the fxaxg/one-api-hub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (328.8 KB), approximately 85.6k tokens, and a symbol index with 170 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!