Jelajahi Sumber

feat: 引入 Prettier 代码格式化工具并集成到 CI/CD

- 安装 prettier 和 eslint-config-prettier 依赖
- 新增 .prettierrc.json 和 .prettierignore 配置文件
- 更新 ESLint 配置以避免与 Prettier 规则冲突
- 添加 format 和 format:check npm 脚本
- 集成到 release.yml: VERSION 更新和代码格式化在同一 commit
- 自动格式化 137 个文件,统一代码风格

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
ding113 4 bulan lalu
induk
melakukan
b4857987b7
100 mengubah file dengan 2668 tambahan dan 2096 penghapusan
  1. 9 1
      .github/CI_CD_SETUP.md
  2. 41 41
      .github/workflows/pr-check.yml
  3. 342 314
      .github/workflows/release.yml
  4. 1 4
      .mcp.json
  5. 55 0
      .prettierignore
  6. 12 0
      .prettierrc.json
  7. 10 4
      CLAUDE.md
  8. 18 5
      README.md
  9. 1 1
      components.json
  10. 7 7
      drizzle.config.ts
  11. 1 1
      eslint.config.mjs
  12. 4 7
      next.config.ts
  13. 6 0
      package.json
  14. 207 0
      pnpm-lock.yaml
  15. 9 8
      src/actions/active-sessions.ts
  16. 3 2
      src/actions/concurrent-sessions.ts
  17. 57 40
      src/actions/keys.ts
  18. 15 20
      src/actions/model-prices.ts
  19. 74 62
      src/actions/providers.ts
  20. 2 1
      src/actions/proxy-status.ts
  21. 28 31
      src/actions/statistics.ts
  22. 3 2
      src/actions/system-config.ts
  23. 8 8
      src/actions/usage-logs.ts
  24. 31 40
      src/actions/users.ts
  25. 52 0
      src/app/api/admin/log-level/route.ts
  26. 8 16
      src/app/api/auth/login/route.ts
  27. 3 3
      src/app/api/auth/logout/route.ts
  28. 9 11
      src/app/api/leaderboard/route.ts
  29. 4 9
      src/app/api/proxy-status/route.ts
  30. 10 9
      src/app/api/version/route.ts
  31. 1 5
      src/app/dashboard/_components/dashboard-nav.tsx
  32. 160 187
      src/app/dashboard/_components/statistics/chart.tsx
  33. 6 1
      src/app/dashboard/_components/statistics/time-range-selector.tsx
  34. 3 12
      src/app/dashboard/_components/statistics/wrapper.tsx
  35. 11 11
      src/app/dashboard/_components/user-menu.tsx
  36. 5 1
      src/app/dashboard/_components/user/add-user-dialog.tsx
  37. 11 11
      src/app/dashboard/_components/user/forms/add-key-form.tsx
  38. 15 10
      src/app/dashboard/_components/user/forms/delete-key-confirm.tsx
  39. 15 10
      src/app/dashboard/_components/user/forms/delete-user-confirm.tsx
  40. 11 11
      src/app/dashboard/_components/user/forms/edit-key-form.tsx
  41. 5 5
      src/app/dashboard/_components/user/forms/user-form.tsx
  42. 2 3
      src/app/dashboard/_components/user/key-actions.tsx
  43. 22 29
      src/app/dashboard/_components/user/key-list-header.tsx
  44. 23 36
      src/app/dashboard/_components/user/key-list.tsx
  45. 3 3
      src/app/dashboard/_components/user/user-key-manager.tsx
  46. 3 12
      src/app/dashboard/_components/user/user-list.tsx
  47. 1 3
      src/app/dashboard/layout.tsx
  48. 2 9
      src/app/dashboard/leaderboard/_components/leaderboard-table.tsx
  49. 1 4
      src/app/dashboard/leaderboard/_components/leaderboard-view.tsx
  50. 1 4
      src/app/dashboard/leaderboard/page.tsx
  51. 5 10
      src/app/dashboard/page.tsx
  52. 13 34
      src/app/dashboard/sessions/[sessionId]/messages/page.tsx
  53. 17 15
      src/app/dashboard/sessions/_components/active-sessions-table.tsx
  54. 17 22
      src/app/dashboard/sessions/_components/session-messages-dialog.tsx
  55. 5 18
      src/app/dashboard/sessions/page.tsx
  56. 0 1
      src/app/globals.css
  57. 19 19
      src/app/login/page.tsx
  58. 1 3
      src/app/settings/_components/settings-page-header.tsx
  59. 1 0
      src/app/settings/_lib/nav-items.ts
  60. 6 2
      src/app/settings/config/_components/system-settings-form.tsx
  61. 1 4
      src/app/settings/config/page.tsx
  62. 1 3
      src/app/settings/layout.tsx
  63. 1 6
      src/app/settings/prices/_components/sync-litellm-button.tsx
  64. 104 108
      src/app/settings/prices/_components/upload-price-dialog.tsx
  65. 4 9
      src/app/settings/prices/page.tsx
  66. 52 32
      src/app/settings/providers/_components/forms/provider-form.tsx
  67. 85 54
      src/app/settings/providers/_components/hooks/use-provider-edit.ts
  68. 115 47
      src/app/settings/providers/_components/provider-list-item.tsx
  69. 12 11
      src/app/settings/providers/_components/provider-list.tsx
  70. 12 13
      src/app/settings/providers/_components/provider-manager.tsx
  71. 58 44
      src/app/settings/providers/_components/scheduling-rules-dialog.tsx
  72. 2 5
      src/app/settings/providers/page.tsx
  73. 10 14
      src/app/usage-doc/layout.tsx
  74. 233 106
      src/app/usage-doc/page.tsx
  75. 3 3
      src/app/v1/[...route]/route.ts
  76. 62 48
      src/app/v1/_lib/codex/chat-completions-handler.ts
  77. 12 11
      src/app/v1/_lib/codex/codex-cli-adapter.ts
  78. 3 3
      src/app/v1/_lib/codex/constants/codex-cli-instructions.ts
  79. 16 16
      src/app/v1/_lib/codex/transformers/request.ts
  80. 19 31
      src/app/v1/_lib/codex/transformers/response.ts
  81. 53 62
      src/app/v1/_lib/codex/transformers/stream.ts
  82. 12 12
      src/app/v1/_lib/codex/types/compatible.ts
  83. 36 36
      src/app/v1/_lib/codex/types/response.ts
  84. 13 14
      src/app/v1/_lib/headers.ts
  85. 2 1
      src/app/v1/_lib/proxy-handler.ts
  86. 2 1
      src/app/v1/_lib/proxy/error-handler.ts
  87. 18 16
      src/app/v1/_lib/proxy/errors.ts
  88. 67 47
      src/app/v1/_lib/proxy/forwarder.ts
  89. 13 4
      src/app/v1/_lib/proxy/logger.ts
  90. 10 3
      src/app/v1/_lib/proxy/message-service.ts
  91. 4 8
      src/app/v1/_lib/proxy/model-redirector.ts
  92. 98 58
      src/app/v1/_lib/proxy/provider-selector.ts
  93. 12 11
      src/app/v1/_lib/proxy/rate-limit-guard.ts
  94. 51 34
      src/app/v1/_lib/proxy/response-handler.ts
  95. 4 4
      src/app/v1/_lib/proxy/responses.ts
  96. 10 13
      src/app/v1/_lib/proxy/session-guard.ts
  97. 17 13
      src/app/v1/_lib/proxy/session.ts
  98. 6 7
      src/app/v1/_lib/url.ts
  99. 5 12
      src/components/customs/concurrent-sessions-card.tsx
  100. 15 34
      src/components/customs/version-checker.tsx

+ 9 - 1
.github/CI_CD_SETUP.md

@@ -5,11 +5,13 @@
 本项目包含两个独立的 GitHub Actions 工作流:
 
 ### 1. PR 构建检查 (`pr-check.yml`)
+
 - **触发条件**:向 `dev` 或 `main` 分支提交 Pull Request
 - **功能**:构建 Docker 镜像但不推送,用于验证代码可构建性
 - **作用**:作为合并前的质量门控
 
 ### 2. 版本发布 (`release.yml`)
+
 - **触发条件**:在 `main` 分支上创建符合 `x.x.x` 格式的标签
 - **功能**:构建并推送 Docker 镜像到 DockerHub
 - **推送标签**:版本标签 + `latest` 标签
@@ -24,6 +26,7 @@ DOCKERHUB_TOKEN = <your-dockerhub-access-token>
 ```
 
 ### 获取 DockerHub Token
+
 1. 登录 [Docker Hub](https://hub.docker.com)
 2. Account Settings → Security
 3. New Access Token → 创建具有 `Read & Write` 权限的 Token
@@ -71,6 +74,7 @@ DOCKERHUB_TOKEN = <your-dockerhub-access-token>
 ## 🔄 工作流程示例
 
 ### 1. 功能开发流程
+
 ```bash
 # 1. 创建功能分支
 git checkout -b feature/new-feature
@@ -87,6 +91,7 @@ git push origin feature/new-feature
 ```
 
 ### 2. 发布流程
+
 ```bash
 # 1. 从 dev 合并到 main
 git checkout main
@@ -135,16 +140,19 @@ docker run -d \
 ## 🚨 故障排除
 
 ### PR 构建失败
+
 - 检查 Dockerfile 语法
 - 查看 Actions 日志中的错误信息
 - 确保所有依赖正确安装
 
 ### 无法推送到 DockerHub
+
 - 验证 Secrets 配置正确
 - 检查 DockerHub Token 权限
 - 确认 DockerHub 仓库名称正确
 
 ### 标签发布未触发
+
 - 确保标签格式正确(`x.x.x`)
 - 确认标签在 `main` 分支上创建
-- 检查 Actions 是否被禁用
+- 检查 Actions 是否被禁用

+ 41 - 41
.github/workflows/pr-check.yml

@@ -4,7 +4,7 @@ name: PR Build Check
 on:
   pull_request:
     branches:
-      - main  # 所有 PR 都合并到 main 分支
+      - main # 所有 PR 都合并到 main 分支
     types: [opened, synchronize, reopened]
 
 env:
@@ -18,43 +18,43 @@ jobs:
     name: Docker Build Test
 
     steps:
-    - name: 📥 Checkout repository
-      uses: actions/checkout@v4
-
-    - name: 🔧 Set up Docker Buildx
-      uses: docker/setup-buildx-action@v3
-
-    - name: 📋 Extract metadata
-      id: meta
-      uses: docker/metadata-action@v5
-      with:
-        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
-        tags: |
-          type=ref,event=pr
-          type=sha,prefix=pr-
-
-    - name: 🏗️ Build Docker image (Test Only - No Push)
-      uses: docker/build-push-action@v5
-      with:
-        context: .
-        file: ./deploy/Dockerfile
-        platforms: linux/amd64  # PR检查使用单一架构提高速度
-        push: false  # 永远不推送,仅测试构建
-        tags: ${{ steps.meta.outputs.tags }}
-        labels: ${{ steps.meta.outputs.labels }}
-        cache-from: type=gha
-        cache-to: type=gha,mode=max
-
-    - name: Build check passed
-      run: |
-        echo "Docker image build successful!"
-        echo "📦 Image tags that would be used: ${{ steps.meta.outputs.tags }}"
-        echo "🔍 All checks passed - PR is ready for review"
-
-    # 如果构建失败,这个步骤会提供更详细的信息
-    - name: ❌ Build failed diagnostics
-      if: failure()
-      run: |
-        echo "❌ Build or test failed!"
-        echo "Please check the logs above for details."
-        docker images
+      - name: 📥 Checkout repository
+        uses: actions/checkout@v4
+
+      - name: 🔧 Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: 📋 Extract metadata
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+          tags: |
+            type=ref,event=pr
+            type=sha,prefix=pr-
+
+      - name: 🏗️ Build Docker image (Test Only - No Push)
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          file: ./deploy/Dockerfile
+          platforms: linux/amd64 # PR检查使用单一架构提高速度
+          push: false # 永远不推送,仅测试构建
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+
+      - name: Build check passed
+        run: |
+          echo "Docker image build successful!"
+          echo "📦 Image tags that would be used: ${{ steps.meta.outputs.tags }}"
+          echo "🔍 All checks passed - PR is ready for review"
+
+      # 如果构建失败,这个步骤会提供更详细的信息
+      - name: ❌ Build failed diagnostics
+        if: failure()
+        run: |
+          echo "❌ Build or test failed!"
+          echo "Please check the logs above for details."
+          docker images

+ 342 - 314
.github/workflows/release.yml

@@ -15,326 +15,354 @@ jobs:
     # 跳过由GitHub Actions创建的提交,避免死循环
     if: github.event.pusher.name != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
     steps:
-    - name: Checkout code
-      uses: actions/checkout@v4
-      with:
-        fetch-depth: 0
-        token: ${{ secrets.GITHUB_TOKEN }}
-
-    - name: Check if version bump is needed
-      id: check
-      run: |
-        # 检测是否是合并提交
-        PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w)
-        PARENT_COUNT=$((PARENT_COUNT - 1))
-        echo "Parent count: $PARENT_COUNT"
-
-        if [ "$PARENT_COUNT" -gt 1 ]; then
-          # 合并提交:获取合并进来的所有文件变更
-          echo "Detected merge commit, getting all merged changes"
-          # 获取合并基准点
-          MERGE_BASE=$(git merge-base HEAD^1 HEAD^2 2>/dev/null || echo "")
-          if [ -n "$MERGE_BASE" ]; then
-            # 获取从合并基准到 HEAD 的所有变更
-            CHANGED_FILES=$(git diff --name-only $MERGE_BASE..HEAD)
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Check if version bump is needed
+        id: check
+        run: |
+          # 检测是否是合并提交
+          PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w)
+          PARENT_COUNT=$((PARENT_COUNT - 1))
+          echo "Parent count: $PARENT_COUNT"
+
+          if [ "$PARENT_COUNT" -gt 1 ]; then
+            # 合并提交:获取合并进来的所有文件变更
+            echo "Detected merge commit, getting all merged changes"
+            # 获取合并基准点
+            MERGE_BASE=$(git merge-base HEAD^1 HEAD^2 2>/dev/null || echo "")
+            if [ -n "$MERGE_BASE" ]; then
+              # 获取从合并基准到 HEAD 的所有变更
+              CHANGED_FILES=$(git diff --name-only $MERGE_BASE..HEAD)
+            else
+              # 如果无法获取合并基准,使用第二个父提交
+              CHANGED_FILES=$(git diff --name-only HEAD^2..HEAD)
+            fi
+          else
+            # 普通提交:获取相对于上一个提交的变更
+            CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD)
+          fi
+
+          echo "Changed files:"
+          echo "$CHANGED_FILES"
+
+          # 检查是否只有无关文件(.md, docs/, .github/等)
+          SIGNIFICANT_CHANGES=false
+          while IFS= read -r file; do
+            # 跳过空行
+            [ -z "$file" ] && continue
+
+            # 检查是否是需要忽略的文件
+            if [[ ! "$file" =~ \.(md|txt)$ ]] &&
+               [[ ! "$file" =~ ^docs/ ]] &&
+               [[ ! "$file" =~ ^\.github/workflows/ ]] &&
+               [[ "$file" != "VERSION" ]] &&
+               [[ "$file" != ".gitignore" ]] &&
+               [[ "$file" != "LICENSE" ]]; then
+              echo "Found significant change in: $file"
+              SIGNIFICANT_CHANGES=true
+              break
+            fi
+          done <<< "$CHANGED_FILES"
+
+          if [ "$SIGNIFICANT_CHANGES" = true ]; then
+            echo "Significant changes detected, version bump needed"
+            echo "needs_bump=true" >> $GITHUB_OUTPUT
+          else
+            echo "No significant changes, skipping version bump"
+            echo "needs_bump=false" >> $GITHUB_OUTPUT
+          fi
+
+      - name: Get current version
+        if: steps.check.outputs.needs_bump == 'true'
+        id: get_version
+        run: |
+          # 获取最新的tag版本
+          LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
+          echo "Latest tag: $LATEST_TAG"
+          TAG_VERSION=${LATEST_TAG#v}
+
+          # 获取VERSION文件中的版本
+          FILE_VERSION=$(cat VERSION | tr -d '[:space:]')
+          echo "VERSION file: $FILE_VERSION"
+
+          # 比较tag版本和文件版本,取较大值
+          function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
+
+          if version_gt "$FILE_VERSION" "$TAG_VERSION"; then
+            VERSION="$FILE_VERSION"
+            echo "Using VERSION file: $VERSION (newer than tag)"
           else
-            # 如果无法获取合并基准,使用第二个父提交
-            CHANGED_FILES=$(git diff --name-only HEAD^2..HEAD)
+            VERSION="$TAG_VERSION"
+            echo "Using tag version: $VERSION (newer or equal to file)"
           fi
-        else
-          # 普通提交:获取相对于上一个提交的变更
-          CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD)
-        fi
-
-        echo "Changed files:"
-        echo "$CHANGED_FILES"
-
-        # 检查是否只有无关文件(.md, docs/, .github/等)
-        SIGNIFICANT_CHANGES=false
-        while IFS= read -r file; do
-          # 跳过空行
-          [ -z "$file" ] && continue
-
-          # 检查是否是需要忽略的文件
-          if [[ ! "$file" =~ \.(md|txt)$ ]] &&
-             [[ ! "$file" =~ ^docs/ ]] &&
-             [[ ! "$file" =~ ^\.github/workflows/ ]] &&
-             [[ "$file" != "VERSION" ]] &&
-             [[ "$file" != ".gitignore" ]] &&
-             [[ "$file" != "LICENSE" ]]; then
-            echo "Found significant change in: $file"
-            SIGNIFICANT_CHANGES=true
-            break
+
+          echo "Current version: $VERSION"
+          echo "current_version=$VERSION" >> $GITHUB_OUTPUT
+
+      - name: Calculate next version
+        if: steps.check.outputs.needs_bump == 'true'
+        id: next_version
+        run: |
+          VERSION="${{ steps.get_version.outputs.current_version }}"
+
+          # 分割版本号
+          IFS='.' read -r -a version_parts <<< "$VERSION"
+          MAJOR="${version_parts[0]:-0}"
+          MINOR="${version_parts[1]:-0}"
+          PATCH="${version_parts[2]:-0}"
+
+          # 默认递增patch版本
+          NEW_PATCH=$((PATCH + 1))
+          NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
+
+          echo "New version: $NEW_VERSION"
+          echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
+          echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
+
+      - name: Update VERSION file
+        if: steps.check.outputs.needs_bump == 'true'
+        run: |
+          echo "${{ steps.next_version.outputs.new_version }}" > VERSION
+
+      - name: Setup Node.js for formatting
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: actions/setup-node@v4
+        with:
+          node-version: "20"
+
+      - name: Setup pnpm
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: pnpm/action-setup@v4
+        with:
+          version: 9.15.0
+
+      - name: Install dependencies and format code
+        if: steps.check.outputs.needs_bump == 'true'
+        run: |
+          pnpm install --frozen-lockfile
+          pnpm format
+
+      - name: Commit VERSION and formatted code
+        if: steps.check.outputs.needs_bump == 'true'
+        run: |
+          # 配置git
+          git config user.name "github-actions[bot]"
+          git config user.email "github-actions[bot]@users.noreply.github.com"
+
+          # 添加所有更改(VERSION文件 + 格式化后的代码)
+          git add -A
+
+          # 检查是否有更改需要提交
+          if git diff --cached --quiet; then
+            echo "No changes to commit"
+          else
+            # 提交所有更改 - 添加 [skip ci] 以避免再次触发
+            git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]"
+          fi
+
+      - name: Install git-cliff
+        if: steps.check.outputs.needs_bump == 'true'
+        run: |
+          wget -q https://github.com/orhun/git-cliff/releases/download/v1.4.0/git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz
+          tar -xzf git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz
+          chmod +x git-cliff-1.4.0/git-cliff
+          sudo mv git-cliff-1.4.0/git-cliff /usr/local/bin/
+
+      - name: Generate changelog
+        if: steps.check.outputs.needs_bump == 'true'
+        id: changelog
+        run: |
+          # 获取上一个tag以来的更新日志
+          LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
+          if [ -n "$LATEST_TAG" ]; then
+            # 排除VERSION文件的提交
+            CHANGELOG=$(git-cliff --config .github/cliff.toml $LATEST_TAG..HEAD --strip header | grep -v "bump version" | sed '/^$/d' || echo "- 代码优化和改进")
+          else
+            CHANGELOG=$(git-cliff --config .github/cliff.toml --strip header || echo "- 初始版本发布")
           fi
-        done <<< "$CHANGED_FILES"
-
-        if [ "$SIGNIFICANT_CHANGES" = true ]; then
-          echo "Significant changes detected, version bump needed"
-          echo "needs_bump=true" >> $GITHUB_OUTPUT
-        else
-          echo "No significant changes, skipping version bump"
-          echo "needs_bump=false" >> $GITHUB_OUTPUT
-        fi
-
-    - name: Get current version
-      if: steps.check.outputs.needs_bump == 'true'
-      id: get_version
-      run: |
-        # 获取最新的tag版本
-        LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
-        echo "Latest tag: $LATEST_TAG"
-        TAG_VERSION=${LATEST_TAG#v}
-
-        # 获取VERSION文件中的版本
-        FILE_VERSION=$(cat VERSION | tr -d '[:space:]')
-        echo "VERSION file: $FILE_VERSION"
-
-        # 比较tag版本和文件版本,取较大值
-        function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
-
-        if version_gt "$FILE_VERSION" "$TAG_VERSION"; then
-          VERSION="$FILE_VERSION"
-          echo "Using VERSION file: $VERSION (newer than tag)"
-        else
-          VERSION="$TAG_VERSION"
-          echo "Using tag version: $VERSION (newer or equal to file)"
-        fi
-
-        echo "Current version: $VERSION"
-        echo "current_version=$VERSION" >> $GITHUB_OUTPUT
-
-    - name: Calculate next version
-      if: steps.check.outputs.needs_bump == 'true'
-      id: next_version
-      run: |
-        VERSION="${{ steps.get_version.outputs.current_version }}"
-
-        # 分割版本号
-        IFS='.' read -r -a version_parts <<< "$VERSION"
-        MAJOR="${version_parts[0]:-0}"
-        MINOR="${version_parts[1]:-0}"
-        PATCH="${version_parts[2]:-0}"
-
-        # 默认递增patch版本
-        NEW_PATCH=$((PATCH + 1))
-        NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
-
-        echo "New version: $NEW_VERSION"
-        echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
-        echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
-
-    - name: Update VERSION file
-      if: steps.check.outputs.needs_bump == 'true'
-      run: |
-        echo "${{ steps.next_version.outputs.new_version }}" > VERSION
-
-        # 配置git
-        git config user.name "github-actions[bot]"
-        git config user.email "github-actions[bot]@users.noreply.github.com"
-
-        # 提交VERSION文件 - 添加 [skip ci] 以避免再次触发
-        git add VERSION
-        git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]"
-
-    - name: Install git-cliff
-      if: steps.check.outputs.needs_bump == 'true'
-      run: |
-        wget -q https://github.com/orhun/git-cliff/releases/download/v1.4.0/git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz
-        tar -xzf git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz
-        chmod +x git-cliff-1.4.0/git-cliff
-        sudo mv git-cliff-1.4.0/git-cliff /usr/local/bin/
-
-    - name: Generate changelog
-      if: steps.check.outputs.needs_bump == 'true'
-      id: changelog
-      run: |
-        # 获取上一个tag以来的更新日志
-        LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
-        if [ -n "$LATEST_TAG" ]; then
-          # 排除VERSION文件的提交
-          CHANGELOG=$(git-cliff --config .github/cliff.toml $LATEST_TAG..HEAD --strip header | grep -v "bump version" | sed '/^$/d' || echo "- 代码优化和改进")
-        else
-          CHANGELOG=$(git-cliff --config .github/cliff.toml --strip header || echo "- 初始版本发布")
-        fi
-        echo "content<<EOF" >> $GITHUB_OUTPUT
-        echo "$CHANGELOG" >> $GITHUB_OUTPUT
-        echo "EOF" >> $GITHUB_OUTPUT
-
-    - name: Create and push tag
-      if: steps.check.outputs.needs_bump == 'true'
-      run: |
-        NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
-        git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
-        git push origin HEAD:main "$NEW_TAG"
-
-    - name: Prepare image names
-      id: image_names
-      if: steps.check.outputs.needs_bump == 'true'
-      run: |
-        GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-code-hub" | tr '[:upper:]' '[:lower:]')
-
-        echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT"
-
-    - name: Create GitHub Release
-      if: steps.check.outputs.needs_bump == 'true'
-      uses: softprops/action-gh-release@v1
-      with:
-        tag_name: ${{ steps.next_version.outputs.new_tag }}
-        name: Release ${{ steps.next_version.outputs.new_version }}
-        body: |
-          ## 🐳 Docker 镜像
-
-          ```bash
-          docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }}
-          docker pull ${{ steps.image_names.outputs.ghcr_image }}:latest
-          ```
-
-          ## 📦 主要更新
-
-          ${{ steps.changelog.outputs.content }}
-
-          ## 📋 完整更新日志
-
-          查看 [所有版本](https://github.com/${{ github.repository }}/releases)
-        draft: false
-        prerelease: false
-        generate_release_notes: true
-
-    # 自动清理旧的tags和releases(保持最近50个)
-    - name: Cleanup old tags and releases
-      if: steps.check.outputs.needs_bump == 'true'
-      continue-on-error: true
-      env:
-        TAGS_TO_KEEP: 50
-        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      run: |
-        echo "🧹 自动清理旧版本,保持最近 $TAGS_TO_KEEP 个tag..."
-
-        # 获取所有版本tag并按版本号排序(从旧到新)
-        echo "正在获取所有tags..."
-        ALL_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V)
-
-        # 检查是否获取到tags
-        if [ -z "$ALL_TAGS" ]; then
-          echo "⚠️ 未找到任何版本tag"
-          exit 0
-        fi
-
-        TOTAL_COUNT=$(echo "$ALL_TAGS" | wc -l)
-
-        echo "📊 当前tag统计:"
-        echo "- 总数: $TOTAL_COUNT"
-        echo "- 配置保留: $TAGS_TO_KEEP"
-
-        if [ "$TOTAL_COUNT" -gt "$TAGS_TO_KEEP" ]; then
-          DELETE_COUNT=$((TOTAL_COUNT - TAGS_TO_KEEP))
-          echo "- 将要删除: $DELETE_COUNT 个最旧的tag"
-
-          # 获取要删除的tags(最老的)
-          TAGS_TO_DELETE=$(echo "$ALL_TAGS" | head -n "$DELETE_COUNT")
-
-          # 显示将要删除的版本范围
-          OLDEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | head -1)
-          NEWEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | tail -1)
-          echo ""
-          echo "🗑️ 将要删除的版本范围:"
-          echo "- 从: $OLDEST_TO_DELETE"
-          echo "- 到: $NEWEST_TO_DELETE"
-
-          echo ""
-          echo "开始执行删除..."
-          SUCCESS_COUNT=0
-          FAIL_COUNT=0
-
-          for tag in $TAGS_TO_DELETE; do
-            echo -n "  删除 $tag ... "
-
-            # 先检查release是否存在
-            if gh release view "$tag" >/dev/null 2>&1; then
-              # Release存在,删除release会同时删除tag
-              if gh release delete "$tag" --yes --cleanup-tag 2>/dev/null; then
-                echo "(release+tag)"
-                SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
+          echo "content<<EOF" >> $GITHUB_OUTPUT
+          echo "$CHANGELOG" >> $GITHUB_OUTPUT
+          echo "EOF" >> $GITHUB_OUTPUT
+
+      - name: Create and push tag
+        if: steps.check.outputs.needs_bump == 'true'
+        run: |
+          NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
+          git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
+          git push origin HEAD:main "$NEW_TAG"
+
+      - name: Prepare image names
+        id: image_names
+        if: steps.check.outputs.needs_bump == 'true'
+        run: |
+          GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-code-hub" | tr '[:upper:]' '[:lower:]')
+
+          echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT"
+
+      - name: Create GitHub Release
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: softprops/action-gh-release@v1
+        with:
+          tag_name: ${{ steps.next_version.outputs.new_tag }}
+          name: Release ${{ steps.next_version.outputs.new_version }}
+          body: |
+            ## 🐳 Docker 镜像
+
+            ```bash
+            docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }}
+            docker pull ${{ steps.image_names.outputs.ghcr_image }}:latest
+            ```
+
+            ## 📦 主要更新
+
+            ${{ steps.changelog.outputs.content }}
+
+            ## 📋 完整更新日志
+
+            查看 [所有版本](https://github.com/${{ github.repository }}/releases)
+          draft: false
+          prerelease: false
+          generate_release_notes: true
+
+      # 自动清理旧的tags和releases(保持最近50个)
+      - name: Cleanup old tags and releases
+        if: steps.check.outputs.needs_bump == 'true'
+        continue-on-error: true
+        env:
+          TAGS_TO_KEEP: 50
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          echo "🧹 自动清理旧版本,保持最近 $TAGS_TO_KEEP 个tag..."
+
+          # 获取所有版本tag并按版本号排序(从旧到新)
+          echo "正在获取所有tags..."
+          ALL_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V)
+
+          # 检查是否获取到tags
+          if [ -z "$ALL_TAGS" ]; then
+            echo "⚠️ 未找到任何版本tag"
+            exit 0
+          fi
+
+          TOTAL_COUNT=$(echo "$ALL_TAGS" | wc -l)
+
+          echo "📊 当前tag统计:"
+          echo "- 总数: $TOTAL_COUNT"
+          echo "- 配置保留: $TAGS_TO_KEEP"
+
+          if [ "$TOTAL_COUNT" -gt "$TAGS_TO_KEEP" ]; then
+            DELETE_COUNT=$((TOTAL_COUNT - TAGS_TO_KEEP))
+            echo "- 将要删除: $DELETE_COUNT 个最旧的tag"
+
+            # 获取要删除的tags(最老的)
+            TAGS_TO_DELETE=$(echo "$ALL_TAGS" | head -n "$DELETE_COUNT")
+
+            # 显示将要删除的版本范围
+            OLDEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | head -1)
+            NEWEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | tail -1)
+            echo ""
+            echo "🗑️ 将要删除的版本范围:"
+            echo "- 从: $OLDEST_TO_DELETE"
+            echo "- 到: $NEWEST_TO_DELETE"
+
+            echo ""
+            echo "开始执行删除..."
+            SUCCESS_COUNT=0
+            FAIL_COUNT=0
+
+            for tag in $TAGS_TO_DELETE; do
+              echo -n "  删除 $tag ... "
+
+              # 先检查release是否存在
+              if gh release view "$tag" >/dev/null 2>&1; then
+                # Release存在,删除release会同时删除tag
+                if gh release delete "$tag" --yes --cleanup-tag 2>/dev/null; then
+                  echo "(release+tag)"
+                  SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
+                else
+                  echo "❌ (release删除失败)"
+                  FAIL_COUNT=$((FAIL_COUNT + 1))
+                fi
               else
-                echo "❌ (release删除失败)"
-                FAIL_COUNT=$((FAIL_COUNT + 1))
+                # Release不存在,只删除tag
+                if git push origin --delete "$tag" 2>/dev/null; then
+                  echo "(仅tag)"
+                  SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
+                else
+                  echo "⏭️ (已不存在)"
+                  FAIL_COUNT=$((FAIL_COUNT + 1))
+                fi
               fi
+            done
+
+            echo ""
+            echo "📊 清理结果:"
+            echo "- 成功删除: $SUCCESS_COUNT"
+            echo "- 失败/跳过: $FAIL_COUNT"
+
+            # 重新获取并显示保留的版本范围
+            echo ""
+            echo "正在验证清理结果..."
+            REMAINING_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V)
+            REMAINING_COUNT=$(echo "$REMAINING_TAGS" | wc -l)
+            OLDEST=$(echo "$REMAINING_TAGS" | head -1)
+            NEWEST=$(echo "$REMAINING_TAGS" | tail -1)
+
+            echo "清理完成!"
+            echo ""
+            echo "📌 当前保留的版本:"
+            echo "- 最旧版本: $OLDEST"
+            echo "- 最新版本: $NEWEST"
+            echo "- 版本总数: $REMAINING_COUNT"
+
+            # 验证是否达到预期
+            if [ "$REMAINING_COUNT" -le "$TAGS_TO_KEEP" ]; then
+              echo "- 状态: 符合预期(≤$TAGS_TO_KEEP)"
             else
-              # Release不存在,只删除tag
-              if git push origin --delete "$tag" 2>/dev/null; then
-                echo "(仅tag)"
-                SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
-              else
-                echo "⏭️ (已不存在)"
-                FAIL_COUNT=$((FAIL_COUNT + 1))
-              fi
+              echo "- 状态: ⚠️ 超出预期(某些tag可能删除失败)"
             fi
-          done
-
-          echo ""
-          echo "📊 清理结果:"
-          echo "- 成功删除: $SUCCESS_COUNT"
-          echo "- 失败/跳过: $FAIL_COUNT"
-
-          # 重新获取并显示保留的版本范围
-          echo ""
-          echo "正在验证清理结果..."
-          REMAINING_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V)
-          REMAINING_COUNT=$(echo "$REMAINING_TAGS" | wc -l)
-          OLDEST=$(echo "$REMAINING_TAGS" | head -1)
-          NEWEST=$(echo "$REMAINING_TAGS" | tail -1)
-
-          echo "清理完成!"
-          echo ""
-          echo "📌 当前保留的版本:"
-          echo "- 最旧版本: $OLDEST"
-          echo "- 最新版本: $NEWEST"
-          echo "- 版本总数: $REMAINING_COUNT"
-
-          # 验证是否达到预期
-          if [ "$REMAINING_COUNT" -le "$TAGS_TO_KEEP" ]; then
-            echo "- 状态: 符合预期(≤$TAGS_TO_KEEP)"
           else
-            echo "- 状态: ⚠️ 超出预期(某些tag可能删除失败)"
+            echo "当前tag数量($TOTAL_COUNT)未超过限制($TAGS_TO_KEEP),无需清理"
           fi
-        else
-          echo "当前tag数量($TOTAL_COUNT)未超过限制($TAGS_TO_KEEP),无需清理"
-        fi
-
-    # Docker构建步骤
-    - name: Set up QEMU
-      if: steps.check.outputs.needs_bump == 'true'
-      uses: docker/setup-qemu-action@v3
-
-    - name: Set up Docker Buildx
-      if: steps.check.outputs.needs_bump == 'true'
-      uses: docker/setup-buildx-action@v3
-
-    - name: Log in to GitHub Container Registry
-      if: steps.check.outputs.needs_bump == 'true'
-      uses: docker/login-action@v3
-      with:
-        registry: ghcr.io
-        username: ${{ github.repository_owner }}
-        password: ${{ secrets.GITHUB_TOKEN }}
-
-    - name: Build and push Docker image
-      if: steps.check.outputs.needs_bump == 'true'
-      uses: docker/build-push-action@v6
-      with:
-        context: .
-        file: ./deploy/Dockerfile
-        platforms: linux/amd64,linux/arm64
-        push: true
-        build-args: |
-          APP_VERSION=${{ steps.next_version.outputs.new_version }}
-        tags: |
-          ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }}
-          ${{ steps.image_names.outputs.ghcr_image }}:latest
-          ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }}
-        labels: |
-          org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }}
-          org.opencontainers.image.revision=${{ github.sha }}
-          org.opencontainers.image.source=https://github.com/${{ github.repository }}
-        cache-from: type=gha
-        cache-to: type=gha,mode=max
+
+      # Docker构建步骤
+      - name: Set up QEMU
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: docker/setup-qemu-action@v3
+
+      - name: Set up Docker Buildx
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: docker/setup-buildx-action@v3
+
+      - name: Log in to GitHub Container Registry
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Build and push Docker image
+        if: steps.check.outputs.needs_bump == 'true'
+        uses: docker/build-push-action@v6
+        with:
+          context: .
+          file: ./deploy/Dockerfile
+          platforms: linux/amd64,linux/arm64
+          push: true
+          build-args: |
+            APP_VERSION=${{ steps.next_version.outputs.new_version }}
+          tags: |
+            ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }}
+            ${{ steps.image_names.outputs.ghcr_image }}:latest
+            ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }}
+          labels: |
+            org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }}
+            org.opencontainers.image.revision=${{ github.sha }}
+            org.opencontainers.image.source=https://github.com/${{ github.repository }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max

+ 1 - 4
.mcp.json

@@ -8,10 +8,7 @@
     },
     "shadcn": {
       "command": "npx",
-      "args": [
-        "shadcn@latest",
-        "mcp"
-      ]
+      "args": ["shadcn@latest", "mcp"]
     },
     "chrome-devtools": {
       "command": "npx",

+ 55 - 0
.prettierignore

@@ -0,0 +1,55 @@
+# Dependencies
+node_modules
+.pnp
+.pnp.*
+
+# Build outputs
+.next
+out
+build
+dist
+*.tsbuildinfo
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# Environment files
+.env*
+!.env.example
+
+# Database migrations (generated)
+drizzle
+
+# External UI components (shadcn - do not format)
+src/components/ui
+
+# Docker volumes
+data
+
+# IDE and tooling
+.cursor
+.claude
+.serena
+.idea
+.specify
+.vscode
+.DS_Store
+
+# Version control
+.git
+.gitignore
+
+# Package manager
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+
+# Other
+coverage
+.vercel
+*.pem

+ 12 - 0
.prettierrc.json

@@ -0,0 +1,12 @@
+{
+  "semi": true,
+  "trailingComma": "es5",
+  "singleQuote": false,
+  "printWidth": 100,
+  "tabWidth": 2,
+  "useTabs": false,
+  "arrowParens": "always",
+  "endOfLine": "lf",
+  "bracketSpacing": true,
+  "proseWrap": "preserve"
+}

+ 10 - 4
CLAUDE.md

@@ -1,4 +1,3 @@
-
 ## 项目简介
 
 Claude Code Hub 是一个 Claude Code API 代理中转服务平台,用于统一管理多个 CC 服务提供商,提供智能负载均衡、用户权限管理和使用统计功能。
@@ -24,6 +23,7 @@ pnpm db:studio        # 启动 Drizzle Studio 可视化管理界面
 ## 核心架构
 
 ### 技术栈
+
 - **Next.js 15** (App Router) + **React 19** + **TypeScript**
 - **Hono** - 用于 API 路由处理
 - **Drizzle ORM** + **PostgreSQL** - 数据持久化
@@ -31,6 +31,7 @@ pnpm db:studio        # 启动 Drizzle Studio 可视化管理界面
 - **包管理器**: pnpm 9.15.0
 
 ### 目录结构
+
 ```
 src/
 ├── app/                          # Next.js App Router
@@ -55,8 +56,6 @@ src/
 > 每个 `page` 的目录下都可以有 `_components` 目录,用于存储当前 `page` 下封装的组件。
 > 如果有多个页面或者 layout 使用,则应该放在 `src/components/customs/` 目录下,并且根据模块划分不同文件夹。
 
-
-
 ### 代理系统架构
 
 代理请求处理流程 (`src/app/v1/_lib/proxy-handler.ts`) 采用职责链模式:
@@ -79,6 +78,7 @@ src/
 本系统支持 OpenAI Chat Completions API 格式,可直接对接 Codex 类型供应商。核心组件位于 `src/app/v1/_lib/codex/`:
 
 **请求流程**:
+
 ```
 客户端 (OpenAI 格式)
   → /v1/chat/completions 端点
@@ -139,6 +139,7 @@ src/
 ### 数据库 Schema
 
 核心表 (`src/drizzle/schema.ts`):
+
 - **users** - 用户表 (RPM 限制、每日额度)
 - **keys** - API 密钥表
 - **providers** - 上游供应商表 (URL、权重、流量限制)
@@ -157,16 +158,19 @@ src/
 ### 环境配置
 
 必需的环境变量 (`.env.local` 或 `.env`):
+
 - `ADMIN_TOKEN` - 管理员登录令牌
 - `DSN` - PostgreSQL 连接字符串
 - `AUTO_MIGRATE` - 是否自动执行数据库迁移 (默认 true)
 - `NODE_ENV` - 运行环境 (development/production/test)
 
 ### TypeScript 配置
+
 - 路径别名 `@/*` → `./src/*`
 - 严格模式已启用
 
 ### 样式系统
+
 - 使用 Shadcn UI orange 主题
 - 主题变量已在 `globals.css` 中配置
 - 尽量使用 CSS 变量,避免直接修改 `globals.css`
@@ -174,14 +178,16 @@ src/
 ## 开发注意事项
 
 ### MCP 集成
+
 项目配置了 MCP (Model Context Protocol) 数据库工具 (`.mcp.json`),可通过 `@bytebase/dbhub` 进行数据库操作。
 
 ### 数据库迁移
+
 - 修改 schema 后,运行 `pnpm db:generate` 生成迁移文件
 - 生产环境通过 `AUTO_MIGRATE=true` 或手动执行 `pnpm db:migrate`
 
 ### API 认证
+
 - 管理面板使用 `ADMIN_TOKEN` 认证
 - 普通用户则使用名下的用户密钥进行登录
 - 代理 API 使用用户密钥 (`Authorization: Bearer sk-xxx`)调用本服务代理的接口。
-

+ 18 - 5
README.md

@@ -53,19 +53,19 @@
 
 ![首页](/public/readme/首页.png)
 
-*首页面板 - 系统概览与快速访问*
+_首页面板 - 系统概览与快速访问_
 
 ![供应商管理](/public/readme/供应商管理.png)
 
-*供应商管理 - 配置上游服务、权重分配、流量限制*
+_供应商管理 - 配置上游服务、权重分配、流量限制_
 
 ![排行榜](/public/readme/排行榜.png)
 
-*统计排行榜 - 用户和供应商使用情况一目了然*
+_统计排行榜 - 用户和供应商使用情况一目了然_
 
 ![日志](/public/readme/日志.png)
 
-*详细日志记录 - Token 使用、成本计算、调用链追踪*
+_详细日志记录 - Token 使用、成本计算、调用链追踪_
 
 </div>
 
@@ -107,6 +107,7 @@ docker compose ps
 ```
 
 确保三个容器都是 `healthy` 或 `running` 状态:
+
 - `claude-code-hub-db` (PostgreSQL)
 - `claude-code-hub-redis` (Redis)
 - `claude-code-hub-app` (应用服务)
@@ -139,6 +140,7 @@ tar -czf backup_$(date +%Y%m%d_%H%M%S).tar.gz ./data/
 <summary><b>更多管理命令</b></summary>
 
 **服务管理**:
+
 ```bash
 docker compose stop             # 停止服务
 docker compose down             # 停止并删除容器
@@ -146,6 +148,7 @@ docker compose restart redis    # 重启 Redis
 ```
 
 **数据库操作**:
+
 ```bash
 # SQL 备份
 docker exec claude-code-hub-db pg_dump -U postgres claude_code_hub > backup.sql
@@ -155,6 +158,7 @@ docker exec -i claude-code-hub-db psql -U postgres claude_code_hub < backup.sql
 ```
 
 **Redis 操作**:
+
 ```bash
 docker compose exec redis redis-cli ping           # 检查连接
 docker compose exec redis redis-cli info stats     # 查看统计
@@ -163,6 +167,7 @@ docker compose exec redis redis-cli FLUSHALL       # ⚠️ 清空数据
 ```
 
 **完全重置**(⚠️ 会删除所有数据):
+
 ```bash
 docker compose down && rm -rf ./data/ && docker compose up -d
 ```
@@ -187,6 +192,7 @@ docker compose down && rm -rf ./data/ && docker compose up -d
 ### 3️⃣ 创建用户和密钥
 
 **添加用户**:
+
 1. 进入 **设置 → 用户管理**
 2. 点击"添加用户"
 3. 配置:
@@ -196,6 +202,7 @@ docker compose down && rm -rf ./data/ && docker compose up -d
    - 每日额度(USD)
 
 **生成 API 密钥**:
+
 1. 选择用户,点击"生成密钥"
 2. 设置密钥名称
 3. 设置过期时间(可选)
@@ -209,6 +216,7 @@ docker compose down && rm -rf ./data/ && docker compose up -d
 ### 5️⃣ 监控和统计
 
 **仪表盘**页面提供:
+
 - 📈 实时请求量趋势
 - 💰 成本统计和分析
 - 👤 用户活跃度排行
@@ -225,6 +233,7 @@ docker compose down && rm -rf ./data/ && docker compose up -d
 - 导出成本报表
 
 **OpenAI 模型价格配置示例**:
+
 - 模型名称:`gpt-5-codex`
 - 输入价格(USD/M tokens):`0.003`
 - 输出价格(USD/M tokens):`0.006`
@@ -251,7 +260,7 @@ docker compose restart app
 services:
   app:
     ports:
-      - "8080:23000"  # 修改左侧端口为可用端口
+      - "8080:23000" # 修改左侧端口为可用端口
 ```
 
 </details>
@@ -260,11 +269,13 @@ services:
 <summary><b>❓ 数据库迁移失败怎么办?</b></summary>
 
 1. 检查应用日志:
+
    ```bash
    docker compose logs app | grep -i migration
    ```
 
 2. 手动执行迁移:
+
    ```bash
    docker compose exec app pnpm db:migrate
    ```
@@ -300,9 +311,11 @@ Redis 不可用时,限流功能会自动降级,所有请求仍然正常通
 **本服务仅支持 Claude Code 格式的 API 接口。**
 
 **直接支持**:
+
 - 原生提供 Claude Code 格式接口的服务商
 
 **间接支持**(需要先部署 [claude-code-router](https://github.com/zsio/claude-code-router) 进行协议转换):
+
 - 🔄 智谱 AI (GLM)、Moonshot AI (Kimi)、Packy 等
 - 🔄 阿里通义千问、百度文心一言等
 - 🔄 其他非 Claude Code 格式的 AI 服务

+ 1 - 1
components.json

@@ -18,4 +18,4 @@
     "hooks": "@/hooks"
   },
   "iconLibrary": "lucide"
-}
+}

+ 7 - 7
drizzle.config.ts

@@ -1,14 +1,14 @@
-import { config } from 'dotenv';
-import { defineConfig } from 'drizzle-kit';
+import { config } from "dotenv";
+import { defineConfig } from "drizzle-kit";
 
 // Load environment variables from .env.local
-config({ path: '.env.local' });
+config({ path: ".env.local" });
 
 export default defineConfig({
-  out: './drizzle',
-  schema: './src/drizzle/schema.ts',
-  dialect: 'postgresql',
+  out: "./drizzle",
+  schema: "./src/drizzle/schema.ts",
+  dialect: "postgresql",
   dbCredentials: {
     url: process.env.DSN!,
   },
-});
+});

+ 1 - 1
eslint.config.mjs

@@ -10,7 +10,7 @@ const compat = new FlatCompat({
 });
 
 const eslintConfig = [
-  ...compat.extends("next/core-web-vitals", "next/typescript"),
+  ...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
   {
     files: ["src/components/ui/**"],
     rules: {

+ 4 - 7
next.config.ts

@@ -1,10 +1,7 @@
-import type { NextConfig } from 'next'
+import type { NextConfig } from "next";
 
 const nextConfig: NextConfig = {
-  output: 'standalone',
-}
-
-export default nextConfig
-
-
+  output: "standalone",
+};
 
+export default nextConfig;

+ 6 - 0
package.json

@@ -8,6 +8,8 @@
     "start": "next start",
     "lint": "next lint",
     "typecheck": "tsc -p tsconfig.json --noEmit",
+    "format": "prettier --write .",
+    "format:check": "prettier --check .",
     "cui": "npx cui-server --host 0.0.0.0 --port 30000 --token a7564bc8882aa9a2d25d8b4ea6ea1e2e",
     "db:generate": "drizzle-kit generate",
     "db:migrate": "drizzle-kit migrate",
@@ -40,6 +42,8 @@
     "lucide-react": "^0.544.0",
     "next": "15.4.6",
     "next-themes": "^0.4.6",
+    "pino": "^10.1.0",
+    "pino-pretty": "^13.1.2",
     "postgres": "^3.4.7",
     "react": "19.1.0",
     "react-dom": "19.1.0",
@@ -61,6 +65,8 @@
     "drizzle-kit": "^0.31.4",
     "eslint": "^9.35.0",
     "eslint-config-next": "15.4.6",
+    "eslint-config-prettier": "^10.1.8",
+    "prettier": "^3.6.2",
     "tailwindcss": "^4.1.13",
     "typescript": "^5.9.2"
   },

+ 207 - 0
pnpm-lock.yaml

@@ -83,6 +83,12 @@ importers:
       next-themes:
         specifier: ^0.4.6
         version: 0.4.6([email protected]([email protected]))([email protected])
+      pino:
+        specifier: ^10.1.0
+        version: 10.1.0
+      pino-pretty:
+        specifier: ^13.1.2
+        version: 13.1.2
       postgres:
         specifier: ^3.4.7
         version: 3.4.7
@@ -141,6 +147,12 @@ importers:
       eslint-config-next:
         specifier: 15.4.6
         version: 15.4.6([email protected]([email protected]))([email protected])
+      eslint-config-prettier:
+        specifier: ^10.1.8
+        version: 10.1.8([email protected]([email protected]))
+      prettier:
+        specifier: ^3.6.2
+        version: 3.6.2
       tailwindcss:
         specifier: ^4.1.13
         version: 4.1.13
@@ -766,6 +778,9 @@ packages:
     resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
     engines: {node: '>=12.4.0'}
 
+  '@pinojs/[email protected]':
+    resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
 
@@ -1592,6 +1607,10 @@ packages:
     resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
     engines: {node: '>= 0.4'}
 
+  [email protected]:
+    resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+    engines: {node: '>=8.0.0'}
+
   [email protected]:
     resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
     engines: {node: '>= 0.4'}
@@ -1680,6 +1699,9 @@ packages:
     resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
     engines: {node: '>=12.5.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+
   [email protected]:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
@@ -1752,6 +1774,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
+
   [email protected]:
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
     peerDependencies:
@@ -1908,6 +1933,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
   [email protected]:
     resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
     engines: {node: '>=10.13.0'}
@@ -1972,6 +2000,12 @@ packages:
       typescript:
         optional: true
 
+  [email protected]:
+    resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
+    hasBin: true
+    peerDependencies:
+      eslint: '>=7.0.0'
+
   [email protected]:
     resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
 
@@ -2082,6 +2116,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
+
   [email protected]:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
@@ -2103,6 +2140,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+
   [email protected]:
     resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
 
@@ -2219,6 +2259,9 @@ packages:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
 
+  [email protected]:
+    resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
+
   [email protected]:
     resolution: {integrity: sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==}
     engines: {node: '>=16.9.0'}
@@ -2371,6 +2414,10 @@ packages:
     resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
     hasBin: true
 
+  [email protected]:
+    resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
+    engines: {node: '>=10'}
+
   [email protected]:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -2611,6 +2658,13 @@ packages:
     resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
     engines: {node: '>= 0.4'}
 
+  [email protected]:
+    resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
+    engines: {node: '>=14.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
   [email protected]:
     resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
@@ -2664,6 +2718,20 @@ packages:
     resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
     engines: {node: '>=12'}
 
+  [email protected]:
+    resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==}
+    hasBin: true
+
   [email protected]:
     resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
     engines: {node: '>= 0.4'}
@@ -2700,9 +2768,20 @@ packages:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
+    engines: {node: '>=14'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
+
   [email protected]:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
+
   [email protected]:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -2710,6 +2789,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
+  [email protected]:
+    resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+
   [email protected]:
     resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
     peerDependencies:
@@ -2767,6 +2849,10 @@ packages:
     resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
     engines: {node: '>=0.10.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+    engines: {node: '>= 12.13.0'}
+
   [email protected]:
     resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
 
@@ -2828,9 +2914,16 @@ packages:
     resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
     engines: {node: '>= 0.4'}
 
+  [email protected]:
+    resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
+    engines: {node: '>=10'}
+
   [email protected]:
     resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
 
+  [email protected]:
+    resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
+
   [email protected]:
     resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
     hasBin: true
@@ -2883,6 +2976,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
+
   [email protected]:
     resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
     peerDependencies:
@@ -2900,6 +2996,10 @@ packages:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+    engines: {node: '>= 10.x'}
+
   [email protected]:
     resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
 
@@ -2941,6 +3041,10 @@ packages:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
 
+  [email protected]:
+    resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
+    engines: {node: '>=14.16'}
+
   [email protected]:
     resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
     engines: {node: '>= 12.0.0'}
@@ -2976,6 +3080,9 @@ packages:
     resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
     engines: {node: '>=18'}
 
+  [email protected]:
+    resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
+
   [email protected]:
     resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
 
@@ -3096,6 +3203,9 @@ packages:
     resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
     engines: {node: '>=0.10.0'}
 
+  [email protected]:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
   [email protected]:
     resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
     engines: {node: '>=0.4'}
@@ -3532,6 +3642,8 @@ snapshots:
 
   '@nolyfill/[email protected]': {}
 
+  '@pinojs/[email protected]': {}
+
   '@radix-ui/[email protected]': {}
 
   '@radix-ui/[email protected]': {}
@@ -4386,6 +4498,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       possible-typed-array-names: 1.1.0
@@ -4473,6 +4587,8 @@ snapshots:
       color-string: 1.9.1
     optional: true
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -4543,6 +4659,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       ms: 2.1.3
@@ -4607,6 +4725,10 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      once: 1.4.0
+
   [email protected]:
     dependencies:
       graceful-fs: 4.2.11
@@ -4796,6 +4918,10 @@ snapshots:
       - eslint-plugin-import-x
       - supports-color
 
+  [email protected]([email protected]([email protected])):
+    dependencies:
+      eslint: 9.36.0([email protected])
+
   [email protected]:
     dependencies:
       debug: 3.2.7
@@ -4975,6 +5101,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}
@@ -4999,6 +5127,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       reusify: 1.1.0
@@ -5117,6 +5247,8 @@ snapshots:
     dependencies:
       function-bind: 1.1.2
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}
@@ -5281,6 +5413,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -5493,6 +5627,12 @@ snapshots:
       define-properties: 1.2.1
       es-object-atoms: 1.1.1
 
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      wrappy: 1.0.2
+
   [email protected]:
     dependencies:
       deep-is: 0.1.4
@@ -5544,6 +5684,42 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      split2: 4.2.0
+
+  [email protected]:
+    dependencies:
+      colorette: 2.0.20
+      dateformat: 4.6.3
+      fast-copy: 3.0.2
+      fast-safe-stringify: 2.1.1
+      help-me: 5.0.0
+      joycon: 3.1.1
+      minimist: 1.2.8
+      on-exit-leak-free: 2.1.2
+      pino-abstract-transport: 2.0.0
+      pump: 3.0.3
+      secure-json-parse: 4.1.0
+      sonic-boom: 4.2.0
+      strip-json-comments: 5.0.3
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@pinojs/redact': 0.4.0
+      atomic-sleep: 1.0.0
+      on-exit-leak-free: 2.1.2
+      pino-abstract-transport: 2.0.0
+      pino-std-serializers: 7.0.0
+      process-warning: 5.0.0
+      quick-format-unescaped: 4.0.4
+      real-require: 0.2.0
+      safe-stable-stringify: 2.5.0
+      sonic-boom: 4.2.0
+      thread-stream: 3.1.0
+
   [email protected]: {}
 
   [email protected]:
@@ -5572,16 +5748,27 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       loose-envify: 1.4.0
       object-assign: 4.1.1
       react-is: 16.13.1
 
+  [email protected]:
+    dependencies:
+      end-of-stream: 1.4.5
+      once: 1.4.0
+
   [email protected]: {}
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]([email protected]):
     dependencies:
       react: 19.1.0
@@ -5637,6 +5824,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       decimal.js-light: 2.5.1
@@ -5721,8 +5910,12 @@ snapshots:
       es-errors: 1.3.0
       is-regex: 1.2.1
 
+  [email protected]: {}
+
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}
@@ -5818,6 +6011,10 @@ snapshots:
       is-arrayish: 0.3.2
     optional: true
 
+  [email protected]:
+    dependencies:
+      atomic-sleep: 1.0.0
+
   [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       react: 19.1.0
@@ -5832,6 +6029,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}
@@ -5895,6 +6094,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]([email protected]):
     dependencies:
       client-only: 0.0.1
@@ -5921,6 +6122,10 @@ snapshots:
       mkdirp: 3.0.1
       yallist: 5.0.0
 
+  [email protected]:
+    dependencies:
+      real-require: 0.2.0
+
   [email protected]: {}
 
   [email protected]: {}
@@ -6108,6 +6313,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}

+ 9 - 8
src/actions/active-sessions.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { SessionManager } from "@/lib/session-manager";
+import { logger } from '@/lib/logger';
 import type { ActionResult } from "./types";
 import type { ActiveSessionInfo } from "@/types/session";
 
@@ -16,10 +17,10 @@ export async function getActiveSessions(): Promise<ActionResult<ActiveSessionInf
       data: sessions,
     };
   } catch (error) {
-    console.error('Failed to get active sessions:', error);
+    logger.error('Failed to get active sessions:', error);
     return {
       ok: false,
-      error: '获取活跃 session 失败',
+      error: "获取活跃 session 失败",
     };
   }
 }
@@ -41,10 +42,10 @@ export async function getAllSessions(): Promise<
       data: sessions,
     };
   } catch (error) {
-    console.error('Failed to get all sessions:', error);
+    logger.error('Failed to get all sessions:', error);
     return {
       ok: false,
-      error: '获取 session 列表失败',
+      error: "获取 session 列表失败",
     };
   }
 }
@@ -59,7 +60,7 @@ export async function getSessionMessages(sessionId: string): Promise<ActionResul
     if (messages === null) {
       return {
         ok: false,
-        error: 'Messages 未存储或已过期',
+        error: "Messages 未存储或已过期",
       };
     }
     return {
@@ -67,10 +68,10 @@ export async function getSessionMessages(sessionId: string): Promise<ActionResul
       data: messages,
     };
   } catch (error) {
-    console.error('Failed to get session messages:', error);
+    logger.error('Failed to get session messages:', error);
     return {
       ok: false,
-      error: '获取 session messages 失败',
+      error: "获取 session messages 失败",
     };
   }
 }
@@ -87,7 +88,7 @@ export async function hasSessionMessages(sessionId: string): Promise<ActionResul
       data: messages !== null,
     };
   } catch (error) {
-    console.error('Failed to check session messages:', error);
+    logger.error('Failed to check session messages:', error);
     return {
       ok: true,
       data: false, // 出错时默认返回 false,避免显示无效按钮

+ 3 - 2
src/actions/concurrent-sessions.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { getActiveConcurrentSessions } from "@/lib/redis";
+import { logger } from '@/lib/logger';
 import type { ActionResult } from "./types";
 
 /**
@@ -14,10 +15,10 @@ export async function getConcurrentSessions(): Promise<ActionResult<number>> {
       data: count,
     };
   } catch (error) {
-    console.error('Failed to get concurrent sessions:', error);
+    logger.error('Failed to get concurrent sessions:', error);
     return {
       ok: false,
-      error: '获取并发数失败',
+      error: "获取并发数失败",
     };
   }
 }

+ 57 - 40
src/actions/keys.ts

@@ -1,9 +1,19 @@
-'use server';
+"use server";
 
 import { revalidatePath } from "next/cache";
+import { logger } from '@/lib/logger';
 import { randomBytes } from "node:crypto";
 import { KeyFormSchema } from "@/lib/validation/schemas";
-import { createKey, updateKey, deleteKey, findActiveKeyByUserIdAndName, findKeyById, countActiveKeysByUser, findKeysWithStatistics, findKeyList } from "@/repository/key";
+import {
+  createKey,
+  updateKey,
+  deleteKey,
+  findActiveKeyByUserIdAndName,
+  findKeyById,
+  countActiveKeysByUser,
+  findKeysWithStatistics,
+  findKeyList,
+} from "@/repository/key";
 import { getSession } from "@/lib/auth";
 import type { ActionResult } from "./types";
 import type { KeyStatistics } from "@/repository/key";
@@ -11,31 +21,36 @@ import type { Key } from "@/types/key";
 
 // 添加密钥
 // 说明:为提升前端可控性,避免直接抛错,返回判别式结果。
-export async function addKey(
-  data: { userId: number; name: string; expiresAt?: string }
-): Promise<ActionResult<{ generatedKey: string; name: string }>> {
+export async function addKey(data: {
+  userId: number;
+  name: string;
+  expiresAt?: string;
+}): Promise<ActionResult<{ generatedKey: string; name: string }>> {
   try {
     // 权限检查:用户只能给自己添加Key,管理员可以给所有人添加Key
     const session = await getSession();
     if (!session) {
-      return { ok: false, error: '未登录' };
+      return { ok: false, error: "未登录" };
     }
-    if (session.user.role !== 'admin' && session.user.id !== data.userId) {
-      return { ok: false, error: '无权限执行此操作' };
+    if (session.user.role !== "admin" && session.user.id !== data.userId) {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     const validatedData = KeyFormSchema.parse({
       name: data.name,
-      expiresAt: data.expiresAt
+      expiresAt: data.expiresAt,
     });
 
     // 检查是否存在同名的生效key
     const existingKey = await findActiveKeyByUserIdAndName(data.userId, validatedData.name);
     if (existingKey) {
-      return { ok: false, error: `名为"${validatedData.name}"的密钥已存在且正在生效中,请使用不同的名称` };
+      return {
+        ok: false,
+        error: `名为"${validatedData.name}"的密钥已存在且正在生效中,请使用不同的名称`,
+      };
     }
 
-    const generatedKey = 'sk-' + randomBytes(16).toString('hex');
+    const generatedKey = "sk-" + randomBytes(16).toString("hex");
 
     await createKey({
       user_id: data.userId,
@@ -49,13 +64,13 @@ export async function addKey(
       limit_concurrent_sessions: validatedData.limitConcurrentSessions,
     });
 
-    revalidatePath('/dashboard');
+    revalidatePath("/dashboard");
 
     // 返回生成的key供前端显示
     return { ok: true, data: { generatedKey, name: validatedData.name } };
   } catch (error) {
-    console.error('添加密钥失败:', error);
-    const message = error instanceof Error ? error.message : '添加密钥失败,请稍后重试';
+    logger.error('添加密钥失败:', error);
+    const message = error instanceof Error ? error.message : "添加密钥失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
@@ -69,16 +84,16 @@ export async function editKey(
     // 权限检查:用户只能编辑自己的Key,管理员可以编辑所有Key
     const session = await getSession();
     if (!session) {
-      return { ok: false, error: '未登录' };
+      return { ok: false, error: "未登录" };
     }
 
     const key = await findKeyById(keyId);
     if (!key) {
-      return { ok: false, error: '密钥不存在' };
+      return { ok: false, error: "密钥不存在" };
     }
 
-    if (session.user.role !== 'admin' && session.user.id !== key.userId) {
-      return { ok: false, error: '无权限执行此操作' };
+    if (session.user.role !== "admin" && session.user.id !== key.userId) {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     const validatedData = KeyFormSchema.parse(data);
@@ -92,11 +107,11 @@ export async function editKey(
       limit_concurrent_sessions: validatedData.limitConcurrentSessions,
     });
 
-    revalidatePath('/dashboard');
+    revalidatePath("/dashboard");
     return { ok: true };
   } catch (error) {
-    console.error('更新密钥失败:', error);
-    const message = error instanceof Error ? error.message : '更新密钥失败,请稍后重试';
+    logger.error('更新密钥失败:', error);
+    const message = error instanceof Error ? error.message : "更新密钥失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
@@ -107,29 +122,29 @@ export async function removeKey(keyId: number): Promise<ActionResult> {
     // 权限检查:用户只能删除自己的Key,管理员可以删除所有Key
     const session = await getSession();
     if (!session) {
-      return { ok: false, error: '未登录' };
+      return { ok: false, error: "未登录" };
     }
 
     const key = await findKeyById(keyId);
     if (!key) {
-      return { ok: false, error: '密钥不存在' };
+      return { ok: false, error: "密钥不存在" };
     }
 
-    if (session.user.role !== 'admin' && session.user.id !== key.userId) {
-      return { ok: false, error: '无权限执行此操作' };
+    if (session.user.role !== "admin" && session.user.id !== key.userId) {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     const activeKeyCount = await countActiveKeysByUser(key.userId);
     if (activeKeyCount <= 1) {
-      return { ok: false, error: '该用户至少需要保留一个可用的密钥,无法删除最后一个密钥' };
+      return { ok: false, error: "该用户至少需要保留一个可用的密钥,无法删除最后一个密钥" };
     }
 
     await deleteKey(keyId);
-    revalidatePath('/dashboard');
+    revalidatePath("/dashboard");
     return { ok: true };
   } catch (error) {
-    console.error('删除密钥失败:', error);
-    const message = error instanceof Error ? error.message : '删除密钥失败,请稍后重试';
+    logger.error('删除密钥失败:', error);
+    const message = error instanceof Error ? error.message : "删除密钥失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
@@ -139,39 +154,41 @@ export async function getKeys(userId: number): Promise<ActionResult<Key[]>> {
   try {
     const session = await getSession();
     if (!session) {
-      return { ok: false, error: '未登录' };
+      return { ok: false, error: "未登录" };
     }
 
     // 权限检查:用户只能获取自己的密钥,管理员可以获取任何用户的密钥
-    if (session.user.role !== 'admin' && session.user.id !== userId) {
-      return { ok: false, error: '无权限执行此操作' };
+    if (session.user.role !== "admin" && session.user.id !== userId) {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     const keys = await findKeyList(userId);
     return { ok: true, data: keys };
   } catch (error) {
-    console.error('获取密钥列表失败:', error);
-    return { ok: false, error: '获取密钥列表失败' };
+    logger.error('获取密钥列表失败:', error);
+    return { ok: false, error: "获取密钥列表失败" };
   }
 }
 
 // 获取用户密钥的统计信息
-export async function getKeysWithStatistics(userId: number): Promise<ActionResult<KeyStatistics[]>> {
+export async function getKeysWithStatistics(
+  userId: number
+): Promise<ActionResult<KeyStatistics[]>> {
   try {
     const session = await getSession();
     if (!session) {
-      return { ok: false, error: '未登录' };
+      return { ok: false, error: "未登录" };
     }
 
     // 权限检查:用户只能获取自己的统计,管理员可以获取任何用户的统计
-    if (session.user.role !== 'admin' && session.user.id !== userId) {
-      return { ok: false, error: '无权限执行此操作' };
+    if (session.user.role !== "admin" && session.user.id !== userId) {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     const stats = await findKeysWithStatistics(userId);
     return { ok: true, data: stats };
   } catch (error) {
-    console.error('获取密钥统计失败:', error);
-    return { ok: false, error: '获取密钥统计失败' };
+    logger.error('获取密钥统计失败:', error);
+    return { ok: false, error: "获取密钥统计失败" };
   }
 }

+ 15 - 20
src/actions/model-prices.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { revalidatePath } from "next/cache";
+import { logger } from '@/lib/logger';
 import { getSession } from "@/lib/auth";
 import {
   findLatestPriceByModel,
@@ -20,10 +21,7 @@ import { getPriceTableJson } from "@/lib/price-sync";
 /**
  * 检查价格数据是否相同
  */
-function isPriceDataEqual(
-  data1: ModelPriceData,
-  data2: ModelPriceData
-): boolean {
+function isPriceDataEqual(data1: ModelPriceData, data2: ModelPriceData): boolean {
   // 深度比较两个价格对象
   return JSON.stringify(data1) === JSON.stringify(data2);
 }
@@ -62,7 +60,7 @@ export async function uploadPriceTable(
         lowerName.startsWith("claude-") ||
         lowerName.startsWith("gpt-") ||
         lowerName.startsWith("o1-") ||
-        lowerName.startsWith("o3-")  // OpenAI 推理模型
+        lowerName.startsWith("o3-") // OpenAI 推理模型
       );
     });
 
@@ -99,7 +97,7 @@ export async function uploadPriceTable(
           result.unchanged.push(modelName);
         }
       } catch (error) {
-        console.error(`处理模型 ${modelName} 失败:`, error);
+        logger.error('处理模型 ${modelName} 失败:', error);
         result.failed.push(modelName);
       }
     }
@@ -109,9 +107,8 @@ export async function uploadPriceTable(
 
     return { ok: true, data: result };
   } catch (error) {
-    console.error("上传价格表失败:", error);
-    const message =
-      error instanceof Error ? error.message : "上传失败,请稍后重试";
+    logger.error('上传价格表失败:', error);
+    const message = error instanceof Error ? error.message : "上传失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
@@ -129,7 +126,7 @@ export async function getModelPrices(): Promise<ModelPrice[]> {
 
     return await findAllLatestPrices();
   } catch (error) {
-    console.error("获取模型价格失败:", error);
+    logger.error('获取模型价格失败:', error);
     return [];
   }
 }
@@ -148,7 +145,7 @@ export async function hasPriceTable(): Promise<boolean> {
 
     return await hasAnyPriceRecords();
   } catch (error) {
-    console.error("检查价格表失败:", error);
+    logger.error('检查价格表失败:', error);
     return false;
   }
 }
@@ -169,16 +166,16 @@ export async function syncLiteLLMPrices(): Promise<ActionResult<PriceUpdateResul
       return { ok: false, error: "无权限执行此操作" };
     }
 
-    console.log('🔄 Starting LiteLLM price sync...');
+    logger.info('🔄 Starting LiteLLM price sync...');
 
     // 获取价格表 JSON(优先 CDN,降级缓存)
     const jsonContent = await getPriceTableJson();
 
     if (!jsonContent) {
-      console.error('❌ Failed to get price table from both CDN and cache');
+      logger.error('❌ Failed to get price table from both CDN and cache');
       return {
         ok: false,
-        error: '无法从 CDN 或缓存获取价格表,请检查网络连接或稍后重试'
+        error: "无法从 CDN 或缓存获取价格表,请检查网络连接或稍后重试",
       };
     }
 
@@ -186,17 +183,15 @@ export async function syncLiteLLMPrices(): Promise<ActionResult<PriceUpdateResul
     const result = await uploadPriceTable(jsonContent);
 
     if (result.ok) {
-      console.log('LiteLLM price sync completed:', result.data);
+      console.log("LiteLLM price sync completed:", result.data);
     } else {
-      console.error('❌ LiteLLM price sync failed:', result.error);
+      logger.error('❌ LiteLLM price sync failed:', { context: result.error });
     }
 
     return result;
   } catch (error) {
-    console.error('❌ Sync LiteLLM prices failed:', error);
-    const message =
-      error instanceof Error ? error.message : '同步失败,请稍后重试';
+    logger.error('❌ Sync LiteLLM prices failed:', error);
+    const message = error instanceof Error ? error.message : "同步失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
-

+ 74 - 62
src/actions/providers.ts

@@ -1,7 +1,14 @@
-'use server';
+"use server";
 
-import { findProviderList, createProvider, updateProvider, deleteProvider, getProviderStatistics } from "@/repository/provider";
+import {
+  findProviderList,
+  createProvider,
+  updateProvider,
+  deleteProvider,
+  getProviderStatistics,
+} from "@/repository/provider";
 import { revalidatePath } from "next/cache";
+import { logger } from '@/lib/logger';
 import { type ProviderDisplay } from "@/types/provider";
 import { maskKey } from "@/lib/utils/validation";
 import { getSession } from "@/lib/auth";
@@ -14,10 +21,10 @@ import { debugLog } from "@/lib/utils/debug-logger";
 export async function getProviders(): Promise<ProviderDisplay[]> {
   try {
     const session = await getSession();
-    debugLog('getProviders:session', { hasSession: !!session, role: session?.user.role });
+    debugLog("getProviders:session", { hasSession: !!session, role: session?.user.role });
 
-    if (!session || session.user.role !== 'admin') {
-      debugLog('getProviders:unauthorized', { hasSession: !!session, role: session?.user.role });
+    if (!session || session.user.role !== "admin") {
+      debugLog("getProviders:unauthorized", { hasSession: !!session, role: session?.user.role });
       return [];
     }
 
@@ -25,28 +32,26 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
     const [providers, statistics] = await Promise.all([
       findProviderList(),
       getProviderStatistics().catch((error) => {
-        debugLog('getProviders:statistics_error', {
+        debugLog("getProviders:statistics_error", {
           message: error.message,
           stack: error.stack,
-          name: error.name
+          name: error.name,
         });
-        console.error('获取供应商统计数据失败:', error);
+        logger.error('获取供应商统计数据失败:', error);
         return []; // 统计查询失败时返回空数组,不影响供应商列表显示
       }),
     ]);
 
-    debugLog('getProviders:raw_data', {
+    debugLog("getProviders:raw_data", {
       providerCount: providers.length,
       statisticsCount: statistics.length,
-      providerIds: providers.map(p => p.id)
+      providerIds: providers.map((p) => p.id),
     });
 
     // 将统计数据按 provider_id 索引
-    const statsMap = new Map(
-      statistics.map(stat => [stat.id, stat])
-    );
+    const statsMap = new Map(statistics.map((stat) => [stat.id, stat]));
 
-    const result = providers.map(provider => {
+    const result = providers.map((provider) => {
       const stats = statsMap.get(provider.id);
 
       // 安全处理 last_call_time: 可能是 Date 对象、字符串或其他类型
@@ -55,7 +60,7 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
         if (stats?.last_call_time) {
           if (stats.last_call_time instanceof Date) {
             lastCallTimeStr = stats.last_call_time.toISOString();
-          } else if (typeof stats.last_call_time === 'string') {
+          } else if (typeof stats.last_call_time === "string") {
             // 原生 SQL 查询返回的是字符串,直接使用
             lastCallTimeStr = stats.last_call_time;
           } else {
@@ -67,10 +72,10 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
           }
         }
       } catch (error) {
-        debugLog('getProviders:last_call_time_conversion_error', {
+        debugLog("getProviders:last_call_time_conversion_error", {
           providerId: provider.id,
           rawValue: stats?.last_call_time,
-          error: error instanceof Error ? error.message : String(error)
+          error: error instanceof Error ? error.message : String(error),
         });
         // 转换失败时保持 null,不影响整体数据返回
         lastCallTimeStr = null;
@@ -80,14 +85,14 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
       let createdAtStr: string;
       let updatedAtStr: string;
       try {
-        createdAtStr = provider.createdAt.toISOString().split('T')[0];
-        updatedAtStr = provider.updatedAt.toISOString().split('T')[0];
+        createdAtStr = provider.createdAt.toISOString().split("T")[0];
+        updatedAtStr = provider.updatedAt.toISOString().split("T")[0];
       } catch (error) {
-        debugLog('getProviders:date_conversion_error', {
+        debugLog("getProviders:date_conversion_error", {
           providerId: provider.id,
-          error: error instanceof Error ? error.message : String(error)
+          error: error instanceof Error ? error.message : String(error),
         });
-        createdAtStr = new Date().toISOString().split('T')[0];
+        createdAtStr = new Date().toISOString().split("T")[0];
         updatedAtStr = createdAtStr;
       }
 
@@ -114,21 +119,21 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
         createdAt: createdAtStr,
         updatedAt: updatedAtStr,
         // 统计数据(可能为空)
-        todayTotalCostUsd: stats?.today_cost ?? '0',
+        todayTotalCostUsd: stats?.today_cost ?? "0",
         todayCallCount: stats?.today_calls ?? 0,
         lastCallTime: lastCallTimeStr,
         lastCallModel: stats?.last_call_model ?? null,
       };
     });
 
-    debugLog('getProviders:final_result', { count: result.length });
+    debugLog("getProviders:final_result", { count: result.length });
     return result;
   } catch (error) {
-    debugLog('getProviders:catch_error', {
+    debugLog("getProviders:catch_error", {
       message: error instanceof Error ? error.message : String(error),
-      stack: error instanceof Error ? error.stack : undefined
+      stack: error instanceof Error ? error.stack : undefined,
     });
-    console.error("获取服务商数据失败:", error);
+    logger.error('获取服务商数据失败:', error);
     return [];
   }
 }
@@ -156,14 +161,18 @@ export async function addProvider(data: {
 }): Promise<ActionResult> {
   try {
     const session = await getSession();
-    if (!session || session.user.role !== 'admin') {
-      return { ok: false, error: '无权限执行此操作' };
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
-    debugLog('addProvider:input', { name: data.name, url: data.url, provider_type: data.provider_type });
+    debugLog("addProvider:input", {
+      name: data.name,
+      url: data.url,
+      provider_type: data.provider_type,
+    });
 
     const validated = CreateProviderSchema.parse(data);
-    debugLog('addProvider:validated', { name: validated.name });
+    debugLog("addProvider:validated", { name: validated.name });
 
     const payload = {
       ...validated,
@@ -178,19 +187,19 @@ export async function addProvider(data: {
     };
 
     await createProvider(payload);
-    debugLog('addProvider:created_success', { name: validated.name });
+    debugLog("addProvider:created_success", { name: validated.name });
 
-    revalidatePath('/settings/providers');
-    debugLog('addProvider:revalidated', { path: '/settings/providers' });
+    revalidatePath("/settings/providers");
+    debugLog("addProvider:revalidated", { path: "/settings/providers" });
 
     return { ok: true };
   } catch (error) {
-    debugLog('addProvider:error', {
+    debugLog("addProvider:error", {
       message: error instanceof Error ? error.message : String(error),
-      stack: error instanceof Error ? error.stack : undefined
+      stack: error instanceof Error ? error.stack : undefined,
     });
-    console.error('创建服务商失败:', error);
-    const message = error instanceof Error ? error.message : '创建服务商失败';
+    logger.error('创建服务商失败:', error);
+    const message = error instanceof Error ? error.message : "创建服务商失败";
     return { ok: false, error: message };
   }
 }
@@ -221,17 +230,17 @@ export async function editProvider(
 ): Promise<ActionResult> {
   try {
     const session = await getSession();
-    if (!session || session.user.role !== 'admin') {
-      return { ok: false, error: '无权限执行此操作' };
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     const validated = UpdateProviderSchema.parse(data);
     await updateProvider(providerId, validated);
-    revalidatePath('/settings/providers');
+    revalidatePath("/settings/providers");
     return { ok: true };
   } catch (error) {
-    console.error('更新服务商失败:', error);
-    const message = error instanceof Error ? error.message : '更新服务商失败';
+    logger.error('更新服务商失败:', error);
+    const message = error instanceof Error ? error.message : "更新服务商失败";
     return { ok: false, error: message };
   }
 }
@@ -240,16 +249,16 @@ export async function editProvider(
 export async function removeProvider(providerId: number): Promise<ActionResult> {
   try {
     const session = await getSession();
-    if (!session || session.user.role !== 'admin') {
-      return { ok: false, error: '无权限执行此操作' };
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     await deleteProvider(providerId);
-    revalidatePath('/settings/providers');
+    revalidatePath("/settings/providers");
     return { ok: true };
   } catch (error) {
-    console.error('删除服务商失败:', error);
-    const message = error instanceof Error ? error.message : '删除服务商失败';
+    logger.error('删除服务商失败:', error);
+    const message = error instanceof Error ? error.message : "删除服务商失败";
     return { ok: false, error: message };
   }
 }
@@ -261,20 +270,23 @@ export async function removeProvider(providerId: number): Promise<ActionResult>
 export async function getProvidersHealthStatus() {
   try {
     const session = await getSession();
-    if (!session || session.user.role !== 'admin') {
+    if (!session || session.user.role !== "admin") {
       return {};
     }
 
     const healthStatus = getAllHealthStatus();
 
     // 转换为前端友好的格式
-    const enrichedStatus: Record<number, {
-      circuitState: 'closed' | 'open' | 'half-open';
-      failureCount: number;
-      lastFailureTime: number | null;
-      circuitOpenUntil: number | null;
-      recoveryMinutes: number | null;  // 距离恢复的分钟数
-    }> = {};
+    const enrichedStatus: Record<
+      number,
+      {
+        circuitState: "closed" | "open" | "half-open";
+        failureCount: number;
+        lastFailureTime: number | null;
+        circuitOpenUntil: number | null;
+        recoveryMinutes: number | null; // 距离恢复的分钟数
+      }
+    > = {};
 
     Object.entries(healthStatus).forEach(([providerId, health]) => {
       enrichedStatus[Number(providerId)] = {
@@ -290,7 +302,7 @@ export async function getProvidersHealthStatus() {
 
     return enrichedStatus;
   } catch (error) {
-    console.error('获取熔断器状态失败:', error);
+    logger.error('获取熔断器状态失败:', error);
     return {};
   }
 }
@@ -301,17 +313,17 @@ export async function getProvidersHealthStatus() {
 export async function resetProviderCircuit(providerId: number): Promise<ActionResult> {
   try {
     const session = await getSession();
-    if (!session || session.user.role !== 'admin') {
-      return { ok: false, error: '无权限执行此操作' };
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
     }
 
     resetCircuit(providerId);
-    revalidatePath('/settings/providers');
+    revalidatePath("/settings/providers");
 
     return { ok: true };
   } catch (error) {
-    console.error('重置熔断器失败:', error);
-    const message = error instanceof Error ? error.message : '重置熔断器失败';
+    logger.error('重置熔断器失败:', error);
+    const message = error instanceof Error ? error.message : "重置熔断器失败";
     return { ok: false, error: message };
   }
 }

+ 2 - 1
src/actions/proxy-status.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { getSession } from "@/lib/auth";
+import { logger } from '@/lib/logger';
 import { ProxyStatusTracker } from "@/lib/proxy-status-tracker";
 import type { ProxyStatusResponse } from "@/types/proxy-status";
 import type { ActionResult } from "./types";
@@ -16,7 +17,7 @@ export async function getProxyStatus(): Promise<ActionResult<ProxyStatusResponse
     const status = await tracker.getAllUsersStatus();
     return { ok: true, data: status };
   } catch (error) {
-    console.error("获取代理状态失败:", error);
+    logger.error('获取代理状态失败:', error);
     return { ok: false, error: "获取代理状态失败" };
   }
 }

+ 28 - 31
src/actions/statistics.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { getSession } from "@/lib/auth";
+import { logger } from '@/lib/logger';
 import {
   getUserStatisticsFromDB,
   getActiveUsersFromDB,
@@ -44,27 +45,27 @@ export async function getUserStatistics(
     }
 
     // 获取时间范围配置
-    const rangeConfig = TIME_RANGE_OPTIONS.find(option => option.key === timeRange);
+    const rangeConfig = TIME_RANGE_OPTIONS.find((option) => option.key === timeRange);
     if (!rangeConfig) {
       throw new Error(`Invalid time range: ${timeRange}`);
     }
 
     const settings = await getSystemSettings();
-    const isAdmin = session.user.role === 'admin';
+    const isAdmin = session.user.role === "admin";
 
     // 确定显示模式
-    const mode: 'users' | 'keys' | 'mixed' = isAdmin
-      ? 'users'
+    const mode: "users" | "keys" | "mixed" = isAdmin
+      ? "users"
       : settings.allowGlobalUsageView
-        ? 'mixed'
-        : 'keys';
+        ? "mixed"
+        : "keys";
 
-    const prefix = mode === 'mixed' ? 'key' : mode === 'users' ? 'user' : 'key';
+    const prefix = mode === "mixed" ? "key" : mode === "users" ? "user" : "key";
 
     let statsData: Array<DatabaseStatRow | DatabaseKeyStatRow>;
     let entities: Array<DatabaseUser | DatabaseKey>;
 
-    if (mode === 'users') {
+    if (mode === "users") {
       // Admin: 显示所有用户
       const [userStats, userList] = await Promise.all([
         getUserStatisticsFromDB(timeRange),
@@ -72,7 +73,7 @@ export async function getUserStatistics(
       ]);
       statsData = userStats;
       entities = userList;
-    } else if (mode === 'mixed') {
+    } else if (mode === "mixed") {
       // 非 Admin + allowGlobalUsageView: 自己的密钥明细 + 其他用户汇总
       const [ownKeysList, mixedData] = await Promise.all([
         getActiveKeysForUserFromDB(session.user.id),
@@ -80,16 +81,10 @@ export async function getUserStatistics(
       ]);
 
       // 合并数据:自己的密钥 + 其他用户的虚拟条目
-      statsData = [
-        ...mixedData.ownKeys,
-        ...mixedData.othersAggregate,
-      ];
+      statsData = [...mixedData.ownKeys, ...mixedData.othersAggregate];
 
       // 合并实体列表:自己的密钥 + 其他用户虚拟实体
-      entities = [
-        ...ownKeysList,
-        { id: -1, name: '其他用户' },
-      ];
+      entities = [...ownKeysList, { id: -1, name: "其他用户" }];
     } else {
       // 非 Admin + !allowGlobalUsageView: 仅显示自己的密钥
       const [keyStats, keyList] = await Promise.all([
@@ -106,13 +101,13 @@ export async function getUserStatistics(
     statsData.forEach((row) => {
       // 根据分辨率格式化日期
       let dateStr: string;
-      if (rangeConfig.resolution === 'hour') {
+      if (rangeConfig.resolution === "hour") {
         // 小时分辨率:显示为 "HH:mm" 格式
         const hour = new Date(row.date);
         dateStr = hour.toISOString();
       } else {
         // 天分辨率:显示为 "YYYY-MM-DD" 格式
-        dateStr = new Date(row.date).toISOString().split('T')[0];
+        dateStr = new Date(row.date).toISOString().split("T")[0];
       }
 
       if (!dataByDate.has(dateStr)) {
@@ -123,7 +118,7 @@ export async function getUserStatistics(
 
       const dateData = dataByDate.get(dateStr)!;
 
-      const entityId = 'user_id' in row ? row.user_id : row.key_id;
+      const entityId = "user_id" in row ? row.user_id : row.key_id;
       const entityKey = createDataKey(prefix, entityId);
 
       // 安全地处理大数值,防止精度问题
@@ -137,11 +132,13 @@ export async function getUserStatistics(
 
     const result: UserStatisticsData = {
       chartData: Array.from(dataByDate.values()),
-      users: entities.map((entity): StatisticsUser => ({
-        id: entity.id,
-        name: entity.name || (mode === 'users' ? `User${entity.id}` : `Key${entity.id}`),
-        dataKey: createDataKey(prefix, entity.id),
-      })),
+      users: entities.map(
+        (entity): StatisticsUser => ({
+          id: entity.id,
+          name: entity.name || (mode === "users" ? `User${entity.id}` : `Key${entity.id}`),
+          dataKey: createDataKey(prefix, entity.id),
+        })
+      ),
       timeRange,
       resolution: rangeConfig.resolution,
       mode,
@@ -149,23 +146,23 @@ export async function getUserStatistics(
 
     return {
       ok: true,
-      data: result
+      data: result,
     };
   } catch (error) {
-    console.error('Failed to get user statistics:', error);
+    logger.error('Failed to get user statistics:', error);
 
     // 提供更具体的错误信息
-    const errorMessage = error instanceof Error ? error.message : '未知错误';
-    if (errorMessage.includes('numeric field overflow')) {
+    const errorMessage = error instanceof Error ? error.message : "未知错误";
+    if (errorMessage.includes("numeric field overflow")) {
       return {
         ok: false,
-        error: '数据金额过大,请检查数据库中的费用记录'
+        error: "数据金额过大,请检查数据库中的费用记录",
       };
     }
 
     return {
       ok: false,
-      error: '获取统计数据失败:' + errorMessage
+      error: "获取统计数据失败:" + errorMessage,
     };
   }
 }

+ 3 - 2
src/actions/system-config.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { revalidatePath } from "next/cache";
+import { logger } from '@/lib/logger';
 import { getSystemSettings, updateSystemSettings } from "@/repository/system-config";
 import { getSession } from "@/lib/auth";
 import { UpdateSystemSettingsSchema } from "@/lib/validation/schemas";
@@ -17,7 +18,7 @@ export async function fetchSystemSettings(): Promise<ActionResult<SystemSettings
     const settings = await getSystemSettings();
     return { ok: true, data: settings };
   } catch (error) {
-    console.error("获取系统设置失败:", error);
+    logger.error('获取系统设置失败:', error);
     return { ok: false, error: "获取系统设置失败" };
   }
 }
@@ -44,7 +45,7 @@ export async function saveSystemSettings(formData: {
 
     return { ok: true, data: updated };
   } catch (error) {
-    console.error("更新系统设置失败:", error);
+    logger.error('更新系统设置失败:', error);
     const message = error instanceof Error ? error.message : "更新系统设置失败";
     return { ok: false, error: message };
   }

+ 8 - 8
src/actions/usage-logs.ts

@@ -1,12 +1,13 @@
 "use server";
 
 import { getSession } from "@/lib/auth";
+import { logger } from '@/lib/logger';
 import {
   findUsageLogsWithDetails,
   getUsedModels,
   getUsedStatusCodes,
   type UsageLogFilters,
-  type UsageLogsResult
+  type UsageLogsResult,
 } from "@/repository/usage-logs";
 import type { ActionResult } from "./types";
 
@@ -14,7 +15,7 @@ import type { ActionResult } from "./types";
  * 获取使用日志(根据权限过滤)
  */
 export async function getUsageLogs(
-  filters: Omit<UsageLogFilters, 'userId'>
+  filters: Omit<UsageLogFilters, "userId">
 ): Promise<ActionResult<UsageLogsResult>> {
   try {
     const session = await getSession();
@@ -23,15 +24,14 @@ export async function getUsageLogs(
     }
 
     // 如果不是 admin,强制过滤为当前用户
-    const finalFilters: UsageLogFilters = session.user.role === 'admin'
-      ? filters
-      : { ...filters, userId: session.user.id };
+    const finalFilters: UsageLogFilters =
+      session.user.role === "admin" ? filters : { ...filters, userId: session.user.id };
 
     const result = await findUsageLogsWithDetails(finalFilters);
 
     return { ok: true, data: result };
   } catch (error) {
-    console.error("获取使用日志失败:", error);
+    logger.error('获取使用日志失败:', error);
     const message = error instanceof Error ? error.message : "获取使用日志失败";
     return { ok: false, error: message };
   }
@@ -50,7 +50,7 @@ export async function getModelList(): Promise<ActionResult<string[]>> {
     const models = await getUsedModels();
     return { ok: true, data: models };
   } catch (error) {
-    console.error("获取模型列表失败:", error);
+    logger.error('获取模型列表失败:', error);
     return { ok: false, error: "获取模型列表失败" };
   }
 }
@@ -68,7 +68,7 @@ export async function getStatusCodeList(): Promise<ActionResult<number[]>> {
     const codes = await getUsedStatusCodes();
     return { ok: true, data: codes };
   } catch (error) {
-    console.error("获取状态码列表失败:", error);
+    logger.error('获取状态码列表失败:', error);
     return { ok: false, error: "获取状态码列表失败" };
   }
 }

+ 31 - 40
src/actions/users.ts

@@ -1,11 +1,7 @@
 "use server";
 
-import {
-  findUserList,
-  createUser,
-  updateUser,
-  deleteUser,
-} from "@/repository/user";
+import { findUserList, createUser, updateUser, deleteUser } from "@/repository/user";
+import { logger } from '@/lib/logger';
 import { findKeyList, findKeyUsageToday, findKeysWithStatistics } from "@/repository/key";
 import { revalidatePath } from "next/cache";
 import { randomBytes } from "node:crypto";
@@ -46,16 +42,12 @@ export async function getUsers(): Promise<UserDisplay[]> {
           const [keys, usageRecords, keyStatistics] = await Promise.all([
             findKeyList(user.id),
             findKeyUsageToday(user.id),
-            findKeysWithStatistics(user.id)
+            findKeysWithStatistics(user.id),
           ]);
 
-          const usageMap = new Map(
-            usageRecords.map((item) => [item.keyId, item.totalCost ?? 0])
-          );
+          const usageMap = new Map(usageRecords.map((item) => [item.keyId, item.totalCost ?? 0]));
 
-          const statisticsMap = new Map(
-            keyStatistics.map((stat) => [stat.keyId, stat])
-          );
+          const statisticsMap = new Map(keyStatistics.map((stat) => [stat.keyId, stat]));
 
           return {
             id: user.id,
@@ -75,29 +67,27 @@ export async function getUsers(): Promise<UserDisplay[]> {
                 maskedKey: maskKey(key.key),
                 fullKey: canUserManageKey ? key.key : undefined,
                 canCopy: canUserManageKey,
-                expiresAt: key.expiresAt
-                  ? key.expiresAt.toISOString().split("T")[0]
-                  : "永不过期",
+                expiresAt: key.expiresAt ? key.expiresAt.toISOString().split("T")[0] : "永不过期",
                 status: key.isEnabled ? "enabled" : ("disabled" as const),
                 createdAt: key.createdAt,
-                createdAtFormatted: key.createdAt.toLocaleString('zh-CN', {
-                  year: 'numeric',
-                  month: '2-digit',
-                  day: '2-digit',
-                  hour: '2-digit',
-                  minute: '2-digit',
-                  second: '2-digit'
+                createdAtFormatted: key.createdAt.toLocaleString("zh-CN", {
+                  year: "numeric",
+                  month: "2-digit",
+                  day: "2-digit",
+                  hour: "2-digit",
+                  minute: "2-digit",
+                  second: "2-digit",
                 }),
                 todayUsage: usageMap.get(key.id) ?? 0,
                 todayCallCount: stats?.todayCallCount ?? 0,
                 lastUsedAt: stats?.lastUsedAt ?? null,
                 lastProviderName: stats?.lastProviderName ?? null,
-                modelStats: stats?.modelStats ?? []
+                modelStats: stats?.modelStats ?? [],
               };
             }),
           };
         } catch (error) {
-          console.error(`获取用户 ${user.id} 的密钥失败:`, error);
+          logger.error('获取用户 ${user.id} 的密钥失败:', error);
           return {
             id: user.id,
             name: user.name,
@@ -109,12 +99,12 @@ export async function getUsers(): Promise<UserDisplay[]> {
             keys: [],
           };
         }
-      }),
+      })
     );
 
     return userDisplays;
   } catch (error) {
-    console.error("获取用户数据失败:", error);
+    logger.error('获取用户数据失败:', error);
     return [];
   }
 }
@@ -163,9 +153,8 @@ export async function addUser(data: {
     revalidatePath("/dashboard");
     return { ok: true };
   } catch (error) {
-    console.error("添加用户失败:", error);
-    const message =
-      error instanceof Error ? error.message : "添加用户失败,请稍后重试";
+    logger.error('添加用户失败:', error);
+    const message = error instanceof Error ? error.message : "添加用户失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
@@ -173,7 +162,13 @@ export async function addUser(data: {
 // 更新用户
 export async function editUser(
   userId: number,
-  data: { name?: string; note?: string; providerGroup?: string | null; rpm?: number; dailyQuota?: number },
+  data: {
+    name?: string;
+    note?: string;
+    providerGroup?: string | null;
+    rpm?: number;
+    dailyQuota?: number;
+  }
 ): Promise<ActionResult> {
   try {
     const session = await getSession();
@@ -194,17 +189,14 @@ export async function editUser(
     revalidatePath("/dashboard");
     return { ok: true };
   } catch (error) {
-    console.error("更新用户失败:", error);
-    const message =
-      error instanceof Error ? error.message : "更新用户失败,请稍后重试";
+    logger.error('更新用户失败:', error);
+    const message = error instanceof Error ? error.message : "更新用户失败,请稍后重试";
     return { ok: false, error: message };
   }
 }
 
 // 删除用户
-export async function removeUser(
-  userId: number,
-): Promise<ActionResult> {
+export async function removeUser(userId: number): Promise<ActionResult> {
   try {
     const session = await getSession();
     if (!session || session.user.role !== "admin") {
@@ -215,9 +207,8 @@ export async function removeUser(
     revalidatePath("/dashboard");
     return { ok: true };
   } catch (error) {
-    console.error("删除用户失败:", error);
-    const message =
-      error instanceof Error ? error.message : "删除用户失败,请稍后重试";
+    logger.error('删除用户失败:', error);
+    const message = error instanceof Error ? error.message : "删除用户失败,请稍后重试";
     return { ok: false, error: message };
   }
 }

+ 52 - 0
src/app/api/admin/log-level/route.ts

@@ -0,0 +1,52 @@
+import { getSession } from "@/lib/auth";
+import { logger } from '@/lib/logger';
+import { logger, setLogLevel, getLogLevel, type LogLevel } from "@/lib/logger";
+
+/**
+ * GET /api/admin/log-level
+ * 获取当前日志级别
+ */
+export async function GET() {
+  const session = await getSession();
+
+  if (!session || session.user.role !== "admin") {
+    return new Response("Unauthorized", { status: 401 });
+  }
+
+  return Response.json({
+    level: getLogLevel(),
+  });
+}
+
+/**
+ * POST /api/admin/log-level
+ * 设置新的日志级别
+ *
+ * Body: { level: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' }
+ */
+export async function POST(req: Request) {
+  const session = await getSession();
+
+  if (!session || session.user.role !== "admin") {
+    return new Response("Unauthorized", { status: 401 });
+  }
+
+  try {
+    const { level } = await req.json();
+
+    const validLevels: LogLevel[] = ["fatal", "error", "warn", "info", "debug", "trace"];
+    if (!level || !validLevels.includes(level)) {
+      return Response.json({ error: "无效的日志级别", validLevels }, { status: 400 });
+    }
+
+    setLogLevel(level as LogLevel);
+
+    return Response.json({
+      success: true,
+      level,
+    });
+  } catch (error) {
+    logger.error("设置日志级别失败", { error });
+    return Response.json({ error: "设置失败" }, { status: 500 });
+  }
+}

+ 8 - 16
src/app/api/auth/login/route.ts

@@ -1,23 +1,18 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { validateKey, setAuthCookie } from '@/lib/auth';
+import { NextRequest, NextResponse } from "next/server";
+import { logger } from '@/lib/logger';
+import { validateKey, setAuthCookie } from "@/lib/auth";
 
 export async function POST(request: NextRequest) {
   try {
     const { key } = await request.json();
 
     if (!key) {
-      return NextResponse.json(
-        { error: '请输入 API Key' },
-        { status: 400 }
-      );
+      return NextResponse.json({ error: "请输入 API Key" }, { status: 400 });
     }
 
     const session = await validateKey(key);
     if (!session) {
-      return NextResponse.json(
-        { error: 'API Key 无效或已过期' },
-        { status: 401 }
-      );
+      return NextResponse.json({ error: "API Key 无效或已过期" }, { status: 401 });
     }
 
     // 设置认证 cookie
@@ -33,10 +28,7 @@ export async function POST(request: NextRequest) {
       },
     });
   } catch (error) {
-    console.error('Login error:', error);
-    return NextResponse.json(
-      { error: '登录失败,请稍后重试' },
-      { status: 500 }
-    );
+    logger.error('Login error:', error);
+    return NextResponse.json({ error: "登录失败,请稍后重试" }, { status: 500 });
   }
-}
+}

+ 3 - 3
src/app/api/auth/logout/route.ts

@@ -1,7 +1,7 @@
-import { NextResponse } from 'next/server';
-import { clearAuthCookie } from '@/lib/auth';
+import { NextResponse } from "next/server";
+import { clearAuthCookie } from "@/lib/auth";
 
 export async function POST() {
   await clearAuthCookie();
   return NextResponse.json({ ok: true });
-}
+}

+ 9 - 11
src/app/api/leaderboard/route.ts

@@ -1,4 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
+import { logger } from '@/lib/logger';
 import { findDailyLeaderboard, findMonthlyLeaderboard } from "@/repository/leaderboard";
 import { unstable_cache } from "next/cache";
 
@@ -24,9 +25,10 @@ export async function GET(request: NextRequest) {
 
     // 生成缓存 key(包含日期以确保每天/每月自动刷新)
     const now = new Date();
-    const cacheKey = period === "daily"
-      ? `leaderboard:daily:${now.toISOString().split('T')[0]}`
-      : `leaderboard:monthly:${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
+    const cacheKey =
+      period === "daily"
+        ? `leaderboard:daily:${now.toISOString().split("T")[0]}`
+        : `leaderboard:monthly:${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
 
     // 使用 Next.js unstable_cache 进行缓存
     const getCachedLeaderboard = unstable_cache(
@@ -40,7 +42,7 @@ export async function GET(request: NextRequest) {
       [cacheKey],
       {
         revalidate: 300, // 5 分钟缓存
-        tags: [`leaderboard-${period}`]
+        tags: [`leaderboard-${period}`],
       }
     );
 
@@ -48,15 +50,11 @@ export async function GET(request: NextRequest) {
 
     return NextResponse.json(data, {
       headers: {
-        'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
+        "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
       },
     });
-
   } catch (error) {
-    console.error("获取排行榜失败:", error);
-    return NextResponse.json(
-      { error: "获取排行榜数据失败" },
-      { status: 500 }
-    );
+    logger.error('获取排行榜失败:', error);
+    return NextResponse.json({ error: "获取排行榜数据失败" }, { status: 500 });
   }
 }

+ 4 - 9
src/app/api/proxy-status/route.ts

@@ -1,4 +1,5 @@
 import { NextResponse } from "next/server";
+import { logger } from '@/lib/logger';
 import { ProxyStatusTracker } from "@/lib/proxy-status-tracker";
 import { getSession } from "@/lib/auth";
 
@@ -26,10 +27,7 @@ export async function GET() {
     // 验证用户登录
     const session = await getSession();
     if (!session) {
-      return NextResponse.json(
-        { error: "未授权,请先登录" },
-        { status: 401 }
-      );
+      return NextResponse.json({ error: "未授权,请先登录" }, { status: 401 });
     }
 
     // 获取代理状态
@@ -38,10 +36,7 @@ export async function GET() {
 
     return NextResponse.json(status);
   } catch (error) {
-    console.error("Failed to get proxy status:", error);
-    return NextResponse.json(
-      { error: "获取代理状态失败" },
-      { status: 500 }
-    );
+    logger.error('Failed to get proxy status:', error);
+    return NextResponse.json({ error: "获取代理状态失败" }, { status: 500 });
   }
 }

+ 10 - 9
src/app/api/version/route.ts

@@ -1,8 +1,9 @@
-import { NextResponse } from 'next/server';
-import { APP_VERSION, GITHUB_REPO, compareVersions } from '@/lib/version';
+import { NextResponse } from "next/server";
+import { logger } from '@/lib/logger';
+import { APP_VERSION, GITHUB_REPO, compareVersions } from "@/lib/version";
 
-export const runtime = 'edge';
-export const dynamic = 'force-dynamic';
+export const runtime = "edge";
+export const dynamic = "force-dynamic";
 
 interface GitHubRelease {
   tag_name: string;
@@ -22,8 +23,8 @@ export async function GET() {
       `https://api.github.com/repos/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/releases/latest`,
       {
         headers: {
-          'Accept': 'application/vnd.github.v3+json',
-          'User-Agent': 'claude-code-hub',
+          Accept: "application/vnd.github.v3+json",
+          "User-Agent": "claude-code-hub",
         },
         next: {
           revalidate: 3600, // 缓存 1 小时
@@ -37,7 +38,7 @@ export async function GET() {
           current: APP_VERSION,
           latest: null,
           hasUpdate: false,
-          message: '暂无发布版本',
+          message: "暂无发布版本",
         });
       }
       throw new Error(`GitHub API 错误: ${response.status}`);
@@ -57,13 +58,13 @@ export async function GET() {
       publishedAt: release.published_at,
     });
   } catch (error) {
-    console.error('版本检查失败:', error);
+    logger.error('版本检查失败:', error);
     return NextResponse.json(
       {
         current: APP_VERSION,
         latest: null,
         hasUpdate: false,
-        error: '无法获取最新版本信息',
+        error: "无法获取最新版本信息",
       },
       { status: 500 }
     );

+ 1 - 5
src/app/dashboard/_components/dashboard-nav.tsx

@@ -54,11 +54,7 @@ export function DashboardNav({ items }: DashboardNavProps) {
         }
 
         return (
-          <Link
-            key={item.href}
-            href={item.href}
-            className={className}
-          >
+          <Link key={item.href} href={item.href} className={className}>
             {item.label}
           </Link>
         );

+ 160 - 187
src/app/dashboard/_components/statistics/chart.tsx

@@ -1,22 +1,11 @@
-"use client"
+"use client";
 
 import * as React from "react";
 import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
 
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from "@/components/ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { cn, Decimal, formatCurrency, toDecimal } from "@/lib/utils";
-import {
-  ChartConfig,
-  ChartContainer,
-  ChartLegend,
-  ChartTooltip,
-} from "@/components/ui/chart";
+import { ChartConfig, ChartContainer, ChartLegend, ChartTooltip } from "@/components/ui/chart";
 
 import type { UserStatisticsData, TimeRange } from "@/types/statistics";
 import { TimeRangeSelector } from "./time-range-selector";
@@ -50,8 +39,7 @@ const USER_COLOR_PALETTE = [
 ] as const;
 
 // 根据索引循环分配颜色,避免重复定义数组
-const getUserColor = (index: number) =>
-  USER_COLOR_PALETTE[index % USER_COLOR_PALETTE.length];
+const getUserColor = (index: number) => USER_COLOR_PALETTE[index % USER_COLOR_PALETTE.length];
 
 export interface UserStatisticsChartProps {
   data: UserStatisticsData;
@@ -63,46 +51,46 @@ export interface UserStatisticsChartProps {
  * 展示用户的消费金额和API调用次数
  */
 export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsChartProps) {
-  const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost")
+  const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost");
 
   // 用户选择状态(仅 Admin 用 users 模式时启用)
   const [selectedUserIds, setSelectedUserIds] = React.useState<Set<number>>(
-    () => new Set(data.users.map(u => u.id))
-  )
+    () => new Set(data.users.map((u) => u.id))
+  );
 
   // 重置选择状态(当 data.users 变化时)
   React.useEffect(() => {
-    setSelectedUserIds(new Set(data.users.map(u => u.id)))
-  }, [data.users])
+    setSelectedUserIds(new Set(data.users.map((u) => u.id)));
+  }, [data.users]);
 
-  const isAdminMode = data.mode === 'users'
-  const enableUserFilter = isAdminMode && data.users.length > 1
+  const isAdminMode = data.mode === "users";
+  const enableUserFilter = isAdminMode && data.users.length > 1;
 
   const toggleUserSelection = (userId: number) => {
-    setSelectedUserIds(prev => {
-      const next = new Set(prev)
+    setSelectedUserIds((prev) => {
+      const next = new Set(prev);
       if (next.has(userId)) {
         // 至少保留一个用户
         if (next.size > 1) {
-          next.delete(userId)
+          next.delete(userId);
         }
       } else {
-        next.add(userId)
+        next.add(userId);
       }
-      return next
-    })
-  }
+      return next;
+    });
+  };
 
   const selectAllUsers = () => {
-    setSelectedUserIds(new Set(data.users.map(u => u.id)))
-  }
+    setSelectedUserIds(new Set(data.users.map((u) => u.id)));
+  };
 
   const deselectAllUsers = () => {
     // 保留第一个用户
     if (data.users.length > 0) {
-      setSelectedUserIds(new Set([data.users[0].id]))
+      setSelectedUserIds(new Set([data.users[0].id]));
     }
-  }
+  };
 
   // 动态生成图表配置
   const chartConfig = React.useMemo(() => {
@@ -113,29 +101,29 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
       calls: {
         label: "API调用次数",
       },
-    }
+    };
 
     data.users.forEach((user, index) => {
       config[user.dataKey] = {
         label: user.name,
         color: getUserColor(index),
-      }
-    })
+      };
+    });
 
-    return config
-  }, [data.users])
+    return config;
+  }, [data.users]);
 
   const userMap = React.useMemo(() => {
-    return new Map(data.users.map((user) => [user.dataKey, user]))
-  }, [data.users])
+    return new Map(data.users.map((user) => [user.dataKey, user]));
+  }, [data.users]);
 
   // 过滤可见用户(如果启用过滤)
   const visibleUsers = React.useMemo(() => {
     if (!enableUserFilter) {
-      return data.users
+      return data.users;
     }
-    return data.users.filter(u => selectedUserIds.has(u.id))
-  }, [data.users, selectedUserIds, enableUserFilter])
+    return data.users.filter((u) => selectedUserIds.has(u.id));
+  }, [data.users, selectedUserIds, enableUserFilter]);
 
   const numericChartData = React.useMemo(() => {
     return data.chartData.map((day) => {
@@ -145,13 +133,12 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
       visibleUsers.forEach((user) => {
         const costKey = `${user.dataKey}_cost`;
         const costDecimal = toDecimal(day[costKey]);
-        normalized[costKey] = costDecimal
-          ? Number(costDecimal.toDecimalPlaces(6).toString())
-          : 0;
+        normalized[costKey] = costDecimal ? Number(costDecimal.toDecimalPlaces(6).toString()) : 0;
 
         const callsKey = `${user.dataKey}_calls`;
         const callsValue = day[callsKey];
-        normalized[callsKey] = typeof callsValue === "number" ? callsValue : Number(callsValue ?? 0);
+        normalized[callsKey] =
+          typeof callsValue === "number" ? callsValue : Number(callsValue ?? 0);
       });
 
       return normalized;
@@ -160,134 +147,135 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
 
   // 计算每个用户的总数据(包括所有用户,用于 legend 排序)
   const userTotals = React.useMemo(() => {
-    const totals: Record<string, { cost: Decimal; calls: number }> = {}
+    const totals: Record<string, { cost: Decimal; calls: number }> = {};
 
-    data.users.forEach(user => {
-      totals[user.dataKey] = { cost: new Decimal(0), calls: 0 }
-    })
+    data.users.forEach((user) => {
+      totals[user.dataKey] = { cost: new Decimal(0), calls: 0 };
+    });
 
-    data.chartData.forEach(day => {
-      data.users.forEach(user => {
-        const costValue = toDecimal(day[`${user.dataKey}_cost`])
-        const callsValue = day[`${user.dataKey}_calls`]
+    data.chartData.forEach((day) => {
+      data.users.forEach((user) => {
+        const costValue = toDecimal(day[`${user.dataKey}_cost`]);
+        const callsValue = day[`${user.dataKey}_calls`];
 
         if (costValue) {
-          const current = totals[user.dataKey]
-          current.cost = current.cost.plus(costValue)
+          const current = totals[user.dataKey];
+          current.cost = current.cost.plus(costValue);
         }
 
-        totals[user.dataKey].calls += typeof callsValue === 'number' ? callsValue : Number(callsValue ?? 0)
-      })
-    })
+        totals[user.dataKey].calls +=
+          typeof callsValue === "number" ? callsValue : Number(callsValue ?? 0);
+      });
+    });
 
-    return totals
-  }, [data.chartData, data.users])
+    return totals;
+  }, [data.chartData, data.users]);
 
   // 计算可见用户的总计(用于顶部统计卡片)
   const visibleTotals = React.useMemo(() => {
     const costTotal = data.chartData.reduce((sum, day) => {
       const dayTotal = visibleUsers.reduce((daySum, user) => {
-        const costValue = toDecimal(day[`${user.dataKey}_cost`])
-        return costValue ? daySum.plus(costValue) : daySum
-      }, new Decimal(0))
-      return sum.plus(dayTotal)
-    }, new Decimal(0))
+        const costValue = toDecimal(day[`${user.dataKey}_cost`]);
+        return costValue ? daySum.plus(costValue) : daySum;
+      }, new Decimal(0));
+      return sum.plus(dayTotal);
+    }, new Decimal(0));
 
     const callsTotal = data.chartData.reduce((sum, day) => {
       const dayTotal = visibleUsers.reduce((daySum, user) => {
-        const callsValue = day[`${user.dataKey}_calls`]
-        return daySum + (typeof callsValue === 'number' ? callsValue : 0);
-      }, 0)
-      return sum + dayTotal
-    }, 0)
+        const callsValue = day[`${user.dataKey}_calls`];
+        return daySum + (typeof callsValue === "number" ? callsValue : 0);
+      }, 0);
+      return sum + dayTotal;
+    }, 0);
 
-    return { cost: costTotal, calls: callsTotal }
-  }, [data.chartData, visibleUsers])
+    return { cost: costTotal, calls: callsTotal };
+  }, [data.chartData, visibleUsers]);
 
   const sortedLegendUsers = React.useMemo(() => {
     return data.users
       .map((user, index) => ({ user, index }))
       .sort((a, b) => {
-        const totalsA = userTotals[a.user.dataKey]
-        const totalsB = userTotals[b.user.dataKey]
+        const totalsA = userTotals[a.user.dataKey];
+        const totalsB = userTotals[b.user.dataKey];
         if (!totalsA && !totalsB) {
-          return a.index - b.index
+          return a.index - b.index;
         }
 
-        if (!totalsA) return 1
-        if (!totalsB) return -1
+        if (!totalsA) return 1;
+        if (!totalsB) return -1;
 
         if (activeChart === "cost") {
-          const result = totalsB.cost.comparedTo(totalsA.cost)
-          return result !== 0 ? result : a.index - b.index
+          const result = totalsB.cost.comparedTo(totalsA.cost);
+          return result !== 0 ? result : a.index - b.index;
         }
 
         if (totalsB.calls === totalsA.calls) {
-          return a.index - b.index
+          return a.index - b.index;
         }
 
-        return totalsB.calls - totalsA.calls
-      })
-  }, [data.users, userTotals, activeChart])
+        return totalsB.calls - totalsA.calls;
+      });
+  }, [data.users, userTotals, activeChart]);
 
   // 格式化日期显示(根据分辨率)
   const formatDate = (dateStr: string) => {
-    const date = new Date(dateStr)
-    if (data.resolution === 'hour') {
+    const date = new Date(dateStr);
+    if (data.resolution === "hour") {
       return date.toLocaleTimeString("zh-CN", {
         hour: "2-digit",
         minute: "2-digit",
-      })
+      });
     } else {
       return date.toLocaleDateString("zh-CN", {
         month: "numeric",
         day: "numeric",
-      })
+      });
     }
-  }
+  };
 
   // 格式化tooltip日期
   const formatTooltipDate = (dateStr: string) => {
-    const date = new Date(dateStr)
-    if (data.resolution === 'hour') {
+    const date = new Date(dateStr);
+    if (data.resolution === "hour") {
       return date.toLocaleString("zh-CN", {
         month: "long",
         day: "numeric",
         hour: "2-digit",
         minute: "2-digit",
-      })
+      });
     } else {
       return date.toLocaleDateString("zh-CN", {
         year: "numeric",
         month: "long",
         day: "numeric",
-      })
+      });
     }
-  }
+  };
 
   // 获取时间范围的描述文本
   const getTimeRangeDescription = () => {
     switch (data.timeRange) {
-      case 'today':
-        return '今天的使用情况'
-      case '7days':
-        return '过去 7 天的使用情况'
-      case '30days':
-        return '过去 30 天的使用情况'
+      case "today":
+        return "今天的使用情况";
+      case "7days":
+        return "过去 7 天的使用情况";
+      case "30days":
+        return "过去 30 天的使用情况";
       default:
-        return '使用情况'
+        return "使用情况";
     }
-  }
+  };
 
   const getAggregationLabel = () => {
-    if (data.mode === 'keys') {
-      return '仅显示您名下各密钥的使用统计'
-    } else if (data.mode === 'mixed') {
-      return '展示您的密钥明细和其他用户汇总'
+    if (data.mode === "keys") {
+      return "仅显示您名下各密钥的使用统计";
+    } else if (data.mode === "mixed") {
+      return "展示您的密钥明细和其他用户汇总";
     } else {
-      return '展示所有用户的使用统计'
+      return "展示所有用户的使用统计";
     }
-  }
+  };
 
   return (
     <Card className="gap-0 py-0">
@@ -319,9 +307,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
               className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
               onClick={() => setActiveChart("cost")}
             >
-              <span className="text-muted-foreground text-xs">
-                总消费金额
-              </span>
+              <span className="text-muted-foreground text-xs">总消费金额</span>
               <span className="text-lg leading-none font-bold sm:text-3xl">
                 {formatCurrency(visibleTotals.cost)}
               </span>
@@ -331,9 +317,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
               className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
               onClick={() => setActiveChart("calls")}
             >
-              <span className="text-muted-foreground text-xs">
-                总API调用次数
-              </span>
+              <span className="text-muted-foreground text-xs">总API调用次数</span>
               <span className="text-lg leading-none font-bold sm:text-3xl">
                 {visibleTotals.calls.toLocaleString()}
               </span>
@@ -349,9 +333,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
             className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
             onClick={() => setActiveChart("cost")}
           >
-            <span className="text-muted-foreground text-xs">
-              总消费金额
-            </span>
+            <span className="text-muted-foreground text-xs">总消费金额</span>
             <span className="text-lg leading-none font-bold sm:text-xl">
               {formatCurrency(visibleTotals.cost)}
             </span>
@@ -361,9 +343,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
             className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
             onClick={() => setActiveChart("calls")}
           >
-            <span className="text-muted-foreground text-xs">
-              总API调用次数
-            </span>
+            <span className="text-muted-foreground text-xs">总API调用次数</span>
             <span className="text-lg leading-none font-bold sm:text-xl">
               {visibleTotals.calls.toLocaleString()}
             </span>
@@ -371,10 +351,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
         </div>
       )}
       <CardContent className="px-1 sm:p-6">
-        <ChartContainer
-          config={chartConfig}
-          className="aspect-auto h-[280px] w-full"
-        >
+        <ChartContainer config={chartConfig} className="aspect-auto h-[280px] w-full">
           <AreaChart
             data={numericChartData}
             margin={{
@@ -384,7 +361,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
           >
             <defs>
               {data.users.map((user, index) => {
-                const color = getUserColor(index)
+                const color = getUserColor(index);
                 return (
                   <linearGradient
                     key={user.dataKey}
@@ -394,18 +371,10 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
                     x2="0"
                     y2="1"
                   >
-                    <stop
-                      offset="5%"
-                      stopColor={color}
-                      stopOpacity={0.8}
-                    />
-                    <stop
-                      offset="95%"
-                      stopColor={color}
-                      stopOpacity={0.1}
-                    />
+                    <stop offset="5%" stopColor={color} stopOpacity={0.8} />
+                    <stop offset="95%" stopColor={color} stopOpacity={0.1} />
                   </linearGradient>
-                )
+                );
               })}
             </defs>
             <CartesianGrid vertical={false} />
@@ -422,76 +391,78 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
               tickMargin={8}
               tickFormatter={(value) => {
                 if (activeChart === "cost") {
-                  return formatCurrency(value)
+                  return formatCurrency(value);
                 }
-                return Number(value).toLocaleString()
+                return Number(value).toLocaleString();
               }}
             />
             <ChartTooltip
               cursor={false}
               content={({ active, payload, label }) => {
-                if (!active || !payload || !payload.length) return null
+                if (!active || !payload || !payload.length) return null;
 
-                const filteredPayload = payload.filter(entry => {
+                const filteredPayload = payload.filter((entry) => {
                   const value =
-                    typeof entry.value === "number"
-                      ? entry.value
-                      : Number(entry.value ?? 0)
-                  return !Number.isNaN(value) && value !== 0
-                })
+                    typeof entry.value === "number" ? entry.value : Number(entry.value ?? 0);
+                  return !Number.isNaN(value) && value !== 0;
+                });
 
                 if (!filteredPayload.length) {
                   return (
                     <div className="rounded-lg border bg-background p-3 shadow-sm min-w-[200px]">
-                      <div className="font-medium text-center">
-                        {formatTooltipDate(label)}
-                      </div>
+                      <div className="font-medium text-center">{formatTooltipDate(label)}</div>
                     </div>
-                  )
+                  );
                 }
 
                 return (
                   <div className="rounded-lg border bg-background p-3 shadow-sm min-w-[200px]">
                     <div className="grid gap-2">
-                      <div className="font-medium text-center">
-                        {formatTooltipDate(label)}
-                      </div>
+                      <div className="font-medium text-center">{formatTooltipDate(label)}</div>
                       <div className="grid gap-1.5">
                         {[...filteredPayload]
                           .sort((a, b) => (Number(b.value ?? 0) || 0) - (Number(a.value ?? 0) || 0))
                           .map((entry, index) => {
-                          const baseKey = entry.dataKey?.toString().replace(`_${activeChart}`, '') || ''
-                          const displayUser = userMap.get(baseKey)
-                          const value = typeof entry.value === "number" ? entry.value : Number(entry.value ?? 0)
-                          const color = entry.color
-
-                          return (
-                            <div key={index} className="flex items-center justify-between gap-3 text-sm">
-                              <div className="flex items-center gap-2 min-w-0">
-                                <div
-                                  className="h-2 w-2 rounded-full flex-shrink-0"
-                                  style={{ backgroundColor: color }}
-                                />
-                                <span className="font-medium truncate">{displayUser?.name || baseKey}:</span>
+                            const baseKey =
+                              entry.dataKey?.toString().replace(`_${activeChart}`, "") || "";
+                            const displayUser = userMap.get(baseKey);
+                            const value =
+                              typeof entry.value === "number"
+                                ? entry.value
+                                : Number(entry.value ?? 0);
+                            const color = entry.color;
+
+                            return (
+                              <div
+                                key={index}
+                                className="flex items-center justify-between gap-3 text-sm"
+                              >
+                                <div className="flex items-center gap-2 min-w-0">
+                                  <div
+                                    className="h-2 w-2 rounded-full flex-shrink-0"
+                                    style={{ backgroundColor: color }}
+                                  />
+                                  <span className="font-medium truncate">
+                                    {displayUser?.name || baseKey}:
+                                  </span>
+                                </div>
+                                <span className="ml-auto font-mono flex-shrink-0">
+                                  {activeChart === "cost"
+                                    ? formatCurrency(value)
+                                    : value.toLocaleString()}
+                                </span>
                               </div>
-                              <span className="ml-auto font-mono flex-shrink-0">
-                                {activeChart === "cost"
-                                  ? formatCurrency(value)
-                                  : value.toLocaleString()
-                                }
-                              </span>
-                            </div>
-                          )
-                        })}
+                            );
+                          })}
                       </div>
                     </div>
                   </div>
-                )
+                );
               }}
             />
             {visibleUsers.map((user, index) => {
-              const originalIndex = data.users.findIndex(u => u.id === user.id)
-              const color = getUserColor(originalIndex)
+              const originalIndex = data.users.findIndex((u) => u.id === user.id);
+              const color = getUserColor(originalIndex);
               return (
                 <Area
                   key={user.dataKey}
@@ -502,7 +473,7 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
                   stroke={color}
                   stackId="a"
                 />
-              )
+              );
             })}
             <ChartLegend
               content={() => (
@@ -531,9 +502,12 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
                   )}
                   <div className="flex flex-wrap justify-center gap-1">
                     {sortedLegendUsers.map(({ user, index }) => {
-                      const color = getUserColor(index)
-                      const userTotal = userTotals[user.dataKey] ?? { cost: new Decimal(0), calls: 0 }
-                      const isSelected = selectedUserIds.has(user.id)
+                      const color = getUserColor(index);
+                      const userTotal = userTotals[user.dataKey] ?? {
+                        cost: new Decimal(0),
+                        calls: 0,
+                      };
+                      const isSelected = selectedUserIds.has(user.id);
 
                       return (
                         <div
@@ -562,11 +536,10 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
                           <div className="text-xs font-bold text-foreground">
                             {activeChart === "cost"
                               ? formatCurrency(userTotal.cost)
-                              : userTotal.calls.toLocaleString()
-                            }
+                              : userTotal.calls.toLocaleString()}
                           </div>
                         </div>
-                      )
+                      );
                     })}
                   </div>
                 </div>
@@ -576,5 +549,5 @@ export function UserStatisticsChart({ data, onTimeRangeChange }: UserStatisticsC
         </ChartContainer>
       </CardContent>
     </Card>
-  )
+  );
 }

+ 6 - 1
src/app/dashboard/_components/statistics/time-range-selector.tsx

@@ -15,7 +15,12 @@ interface TimeRangeSelectorProps {
  * 时间范围选择器组件
  * 提供今天、7天、30天的选择
  */
-export function TimeRangeSelector({ value, onChange, className, disabled = false }: TimeRangeSelectorProps) {
+export function TimeRangeSelector({
+  value,
+  onChange,
+  className,
+  disabled = false,
+}: TimeRangeSelectorProps) {
   return (
     <div className={cn("flex flex-wrap ", className)}>
       {TIME_RANGE_OPTIONS.map((option) => (

+ 3 - 12
src/app/dashboard/_components/statistics/wrapper.tsx

@@ -17,7 +17,7 @@ const STATISTICS_REFRESH_INTERVAL = 5000; // 5秒刷新一次
 async function fetchStatistics(timeRange: TimeRange): Promise<UserStatisticsData> {
   const result = await getUserStatistics(timeRange);
   if (!result.ok) {
-    throw new Error(result.error || '获取统计数据失败');
+    throw new Error(result.error || "获取统计数据失败");
   }
   return result.data;
 }
@@ -52,17 +52,8 @@ export function StatisticsWrapper({ initialData }: StatisticsWrapperProps) {
 
   // 如果没有数据,显示空状态
   if (!data) {
-    return (
-      <div className="text-center py-8 text-muted-foreground">
-        暂无统计数据
-      </div>
-    );
+    return <div className="text-center py-8 text-muted-foreground">暂无统计数据</div>;
   }
 
-  return (
-    <UserStatisticsChart
-      data={data}
-      onTimeRangeChange={handleTimeRangeChange}
-    />
-  );
+  return <UserStatisticsChart data={data} onTimeRangeChange={handleTimeRangeChange} />;
 }

+ 11 - 11
src/app/dashboard/_components/user-menu.tsx

@@ -1,9 +1,9 @@
-'use client';
+"use client";
 
-import { useRouter } from 'next/navigation';
-import { Avatar, AvatarFallback } from '@/components/ui/avatar';
-import { Button } from '@/components/ui/button';
-import { LogOut } from 'lucide-react';
+import { useRouter } from "next/navigation";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { LogOut } from "lucide-react";
 
 interface UserMenuProps {
   user: {
@@ -18,18 +18,18 @@ export function UserMenu({ user }: UserMenuProps) {
 
   const handleLogout = () => {
     // 立即跳转到登录页面,避免延迟
-    router.push('/login');
+    router.push("/login");
     // 异步调用登出接口,不等待响应
-    fetch('/api/auth/logout', { method: 'POST' }).then(() => {
+    fetch("/api/auth/logout", { method: "POST" }).then(() => {
       router.refresh();
     });
   };
 
   const getInitials = (name: string) => {
     return name
-      .split(' ')
-      .map(word => word[0])
-      .join('')
+      .split(" ")
+      .map((word) => word[0])
+      .join("")
       .toUpperCase()
       .slice(0, 2);
   };
@@ -55,4 +55,4 @@ export function UserMenu({ user }: UserMenuProps) {
       </Button>
     </div>
   );
-}
+}

+ 5 - 1
src/app/dashboard/_components/user/add-user-dialog.tsx

@@ -14,7 +14,11 @@ interface AddUserDialogProps {
   className?: string;
 }
 
-export function AddUserDialog({ variant = "default", size = "default", className }: AddUserDialogProps) {
+export function AddUserDialog({
+  variant = "default",
+  size = "default",
+  className,
+}: AddUserDialogProps) {
   const [open, setOpen] = useState(false);
   return (
     <Dialog open={open} onOpenChange={setOpen}>

+ 11 - 11
src/app/dashboard/_components/user/forms/add-key-form.tsx

@@ -20,8 +20,8 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
   const form = useZodForm({
     schema: KeyFormSchema,
     defaultValues: {
-      name: '',
-      expiresAt: '',
+      name: "",
+      expiresAt: "",
       limit5hUsd: null,
       limitWeeklyUsd: null,
       limitMonthlyUsd: null,
@@ -36,7 +36,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         const result = await addKey({
           userId: userId!,
           name: data.name,
-          expiresAt: data.expiresAt || undefined
+          expiresAt: data.expiresAt || undefined,
         });
 
         if (!result.ok) {
@@ -60,7 +60,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         const errorMessage = err instanceof Error ? err.message : "创建失败,请稍后重试";
         toast.error(errorMessage);
       }
-    }
+    },
   });
 
   return (
@@ -69,7 +69,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         title: "新增 Key",
         description: "为当前用户创建新的API密钥,Key值将自动生成。",
         submitText: "确认创建",
-        loadingText: "创建中..."
+        loadingText: "创建中...",
       }}
       onSubmit={form.handleSubmit}
       isSubmitting={isPending}
@@ -82,14 +82,14 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         maxLength={64}
         autoFocus
         placeholder="请输入Key名称"
-        {...form.getFieldProps('name')}
+        {...form.getFieldProps("name")}
       />
 
       <DateField
         label="过期时间"
         placeholder="选择过期时间"
         description="留空表示永不过期"
-        {...form.getFieldProps('expiresAt')}
+        {...form.getFieldProps("expiresAt")}
       />
 
       <NumberField
@@ -98,7 +98,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         description="5小时内最大消费金额"
         min={0}
         step={0.01}
-        {...form.getFieldProps('limit5hUsd')}
+        {...form.getFieldProps("limit5hUsd")}
       />
 
       <NumberField
@@ -107,7 +107,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         description="每周最大消费金额"
         min={0}
         step={0.01}
-        {...form.getFieldProps('limitWeeklyUsd')}
+        {...form.getFieldProps("limitWeeklyUsd")}
       />
 
       <NumberField
@@ -116,7 +116,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         description="每月最大消费金额"
         min={0}
         step={0.01}
-        {...form.getFieldProps('limitMonthlyUsd')}
+        {...form.getFieldProps("limitMonthlyUsd")}
       />
 
       <NumberField
@@ -125,7 +125,7 @@ export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
         description="同时运行的对话数量"
         min={0}
         step={1}
-        {...form.getFieldProps('limitConcurrentSessions')}
+        {...form.getFieldProps("limitConcurrentSessions")}
       />
     </DialogFormLayout>
   );

+ 15 - 10
src/app/dashboard/_components/user/forms/delete-key-confirm.tsx

@@ -1,6 +1,12 @@
 "use client";
 import { useTransition } from "react";
-import { DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog";
+import {
+  DialogHeader,
+  DialogTitle,
+  DialogDescription,
+  DialogFooter,
+  DialogClose,
+} from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
 import { useRouter } from "next/navigation";
 import { removeKey } from "@/actions/keys";
@@ -14,7 +20,10 @@ interface DeleteKeyConfirmProps {
   };
 }
 
-export function DeleteKeyConfirm({ keyData, onSuccess }: DeleteKeyConfirmProps & { onSuccess?: () => void }) {
+export function DeleteKeyConfirm({
+  keyData,
+  onSuccess,
+}: DeleteKeyConfirmProps & { onSuccess?: () => void }) {
   const router = useRouter();
   const [isPending, startTransition] = useTransition();
 
@@ -24,14 +33,14 @@ export function DeleteKeyConfirm({ keyData, onSuccess }: DeleteKeyConfirmProps &
       try {
         const res = await removeKey(keyData.id);
         if (!res.ok) {
-          toast.error(res.error || '删除失败');
+          toast.error(res.error || "删除失败");
           return;
         }
         onSuccess?.();
         router.refresh();
       } catch (error) {
         console.error("删除Key失败:", error);
-        toast.error('删除失败,请稍后重试');
+        toast.error("删除失败,请稍后重试");
       }
     });
   };
@@ -48,18 +57,14 @@ export function DeleteKeyConfirm({ keyData, onSuccess }: DeleteKeyConfirmProps &
           此操作无法撤销,删除后所有使用此密钥的应用将无法访问。
         </DialogDescription>
       </DialogHeader>
-      
+
       <DialogFooter>
         <DialogClose asChild>
           <Button type="button" variant="outline" disabled={isPending}>
             取消
           </Button>
         </DialogClose>
-        <Button 
-          variant="destructive" 
-          onClick={handleConfirm}
-          disabled={isPending}
-        >
+        <Button variant="destructive" onClick={handleConfirm} disabled={isPending}>
           {isPending ? "删除中..." : "确认删除"}
         </Button>
       </DialogFooter>

+ 15 - 10
src/app/dashboard/_components/user/forms/delete-user-confirm.tsx

@@ -1,6 +1,12 @@
 "use client";
 import { useTransition } from "react";
-import { DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog";
+import {
+  DialogHeader,
+  DialogTitle,
+  DialogDescription,
+  DialogFooter,
+  DialogClose,
+} from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
 import { useRouter } from "next/navigation";
 import { removeUser } from "@/actions/users";
@@ -14,7 +20,10 @@ interface DeleteUserConfirmProps {
   };
 }
 
-export function DeleteUserConfirm({ user, onSuccess }: DeleteUserConfirmProps & { onSuccess?: () => void }) {
+export function DeleteUserConfirm({
+  user,
+  onSuccess,
+}: DeleteUserConfirmProps & { onSuccess?: () => void }) {
   const router = useRouter();
   const [isPending, startTransition] = useTransition();
 
@@ -24,14 +33,14 @@ export function DeleteUserConfirm({ user, onSuccess }: DeleteUserConfirmProps &
       try {
         const res = await removeUser(user.id);
         if (!res.ok) {
-          toast.error(res.error || '删除失败');
+          toast.error(res.error || "删除失败");
           return;
         }
         onSuccess?.();
         router.refresh();
       } catch (error) {
         console.error("删除用户失败:", error);
-        toast.error('删除失败,请稍后重试');
+        toast.error("删除失败,请稍后重试");
       }
     });
   };
@@ -46,18 +55,14 @@ export function DeleteUserConfirm({ user, onSuccess }: DeleteUserConfirmProps &
           此操作将同时删除该用户的 {user?.keys.length || 0} 个密钥,且无法撤销。
         </DialogDescription>
       </DialogHeader>
-      
+
       <DialogFooter>
         <DialogClose asChild>
           <Button type="button" variant="outline" disabled={isPending}>
             取消
           </Button>
         </DialogClose>
-        <Button 
-          variant="destructive" 
-          onClick={handleConfirm}
-          disabled={isPending}
-        >
+        <Button variant="destructive" onClick={handleConfirm} disabled={isPending}>
           {isPending ? "删除中..." : "确认删除"}
         </Button>
       </DialogFooter>

+ 11 - 11
src/app/dashboard/_components/user/forms/edit-key-form.tsx

@@ -28,7 +28,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
   const formatExpiresAt = (expiresAt: string) => {
     if (!expiresAt || expiresAt === "永不过期") return "";
     try {
-      return new Date(expiresAt).toISOString().split('T')[0];
+      return new Date(expiresAt).toISOString().split("T")[0];
     } catch {
       return "";
     }
@@ -37,7 +37,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
   const form = useZodForm({
     schema: KeyFormSchema,
     defaultValues: {
-      name: keyData?.name || '',
+      name: keyData?.name || "",
       expiresAt: formatExpiresAt(keyData?.expiresAt || ""),
       limit5hUsd: keyData?.limit5hUsd ?? null,
       limitWeeklyUsd: keyData?.limitWeeklyUsd ?? null,
@@ -56,7 +56,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
             expiresAt: data.expiresAt || undefined,
           });
           if (!res.ok) {
-            toast.error(res.error || '保存失败');
+            toast.error(res.error || "保存失败");
             return;
           }
           onSuccess?.();
@@ -66,7 +66,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
           toast.error("保存失败,请稍后重试");
         }
       });
-    }
+    },
   });
 
   return (
@@ -75,7 +75,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
         title: "编辑 Key",
         description: "修改密钥的名称、过期时间和限流配置。",
         submitText: "保存修改",
-        loadingText: "保存中..."
+        loadingText: "保存中...",
       }}
       onSubmit={form.handleSubmit}
       isSubmitting={isPending}
@@ -88,14 +88,14 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
         maxLength={64}
         autoFocus
         placeholder="请输入Key名称"
-        {...form.getFieldProps('name')}
+        {...form.getFieldProps("name")}
       />
 
       <DateField
         label="过期时间"
         placeholder="选择过期时间"
         description="留空表示永不过期"
-        {...form.getFieldProps('expiresAt')}
+        {...form.getFieldProps("expiresAt")}
       />
 
       <NumberField
@@ -104,7 +104,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
         description="5小时内最大消费金额"
         min={0}
         step={0.01}
-        {...form.getFieldProps('limit5hUsd')}
+        {...form.getFieldProps("limit5hUsd")}
       />
 
       <NumberField
@@ -113,7 +113,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
         description="每周最大消费金额"
         min={0}
         step={0.01}
-        {...form.getFieldProps('limitWeeklyUsd')}
+        {...form.getFieldProps("limitWeeklyUsd")}
       />
 
       <NumberField
@@ -122,7 +122,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
         description="每月最大消费金额"
         min={0}
         step={0.01}
-        {...form.getFieldProps('limitMonthlyUsd')}
+        {...form.getFieldProps("limitMonthlyUsd")}
       />
 
       <NumberField
@@ -131,7 +131,7 @@ export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
         description="同时运行的对话数量"
         min={0}
         step={1}
-        {...form.getFieldProps('limitConcurrentSessions')}
+        {...form.getFieldProps("limitConcurrentSessions")}
       />
     </DialogFormLayout>
   );

+ 5 - 5
src/app/dashboard/_components/user/forms/user-form.tsx

@@ -29,11 +29,11 @@ export function UserForm({ user, onSuccess }: UserFormProps) {
   const form = useZodForm({
     schema: CreateUserSchema, // Use CreateUserSchema for both, it has all fields with defaults
     defaultValues: {
-      name: user?.name || '',
-      note: user?.note || '',
+      name: user?.name || "",
+      note: user?.note || "",
       rpm: user?.rpm || USER_DEFAULTS.RPM,
       dailyQuota: user?.dailyQuota || USER_DEFAULTS.DAILY_QUOTA,
-      providerGroup: user?.providerGroup || '',
+      providerGroup: user?.providerGroup || "",
     },
     onSubmit: async (data) => {
       startTransition(async () => {
@@ -66,7 +66,7 @@ export function UserForm({ user, onSuccess }: UserFormProps) {
           onSuccess?.();
           router.refresh();
         } catch (err) {
-          console.error(`${isEdit ? '编辑' : '添加'}用户失败:`, err);
+          console.error(`${isEdit ? "编辑" : "添加"}用户失败:`, err);
           toast.error(isEdit ? "保存失败,请稍后重试" : "创建失败,请稍后重试");
         }
       });
@@ -135,4 +135,4 @@ export function UserForm({ user, onSuccess }: UserFormProps) {
       />
     </DialogFormLayout>
   );
-}
+}

+ 2 - 3
src/app/dashboard/_components/user/key-actions.tsx

@@ -20,9 +20,8 @@ export function KeyActions({ keyData, currentUser, keyOwnerUserId, canDelete }:
   const [openDelete, setOpenDelete] = useState(false);
 
   // 权限检查:只有管理员或Key的拥有者才能编辑/删除
-  const canManageKey = currentUser && (
-    currentUser.role === 'admin' || currentUser.id === keyOwnerUserId
-  );
+  const canManageKey =
+    currentUser && (currentUser.role === "admin" || currentUser.id === keyOwnerUserId);
 
   // 如果没有权限,不显示任何操作按钮
   if (!canManageKey) {

+ 22 - 29
src/app/dashboard/_components/user/key-list-header.tsx

@@ -1,7 +1,15 @@
 "use client";
 import { useMemo, useState } from "react";
 import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog";
 import { ListPlus, Copy, CheckCircle } from "lucide-react";
 import { AddKeyForm } from "./forms/add-key-form";
 import { UserActions } from "./user-actions";
@@ -77,7 +85,8 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
   const [keyResult, setKeyResult] = useState<{ generatedKey: string; name: string } | null>(null);
   const [copied, setCopied] = useState(false);
 
-  const totalTodayUsage = activeUser?.keys.reduce((sum, key) => sum + (key.todayUsage ?? 0), 0) ?? 0;
+  const totalTodayUsage =
+    activeUser?.keys.reduce((sum, key) => sum + (key.todayUsage ?? 0), 0) ?? 0;
 
   const proxyStatusEnabled = Boolean(activeUser);
   const {
@@ -113,24 +122,16 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
     }
 
     if (proxyStatusError) {
-      return (
-        <div className="text-xs text-destructive">
-          代理状态获取失败
-        </div>
-      );
+      return <div className="text-xs text-destructive">代理状态获取失败</div>;
     }
 
     if (!activeUserStatus) {
-      return (
-        <div className="text-xs text-muted-foreground">
-          暂无代理状态
-        </div>
-      );
+      return <div className="text-xs text-muted-foreground">暂无代理状态</div>;
     }
 
-    const activeProviders = Array.from(new Set(
-      activeUserStatus.activeRequests.map((request) => request.providerName)
-    ));
+    const activeProviders = Array.from(
+      new Set(activeUserStatus.activeRequests.map((request) => request.providerName))
+    );
 
     return (
       <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
@@ -138,9 +139,7 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
           <span>活跃请求</span>
           <span className="font-medium text-foreground">{activeUserStatus.activeCount}</span>
           {activeProviders.length > 0 && (
-            <span className="text-muted-foreground">
-              ({activeProviders.join("、")})
-            </span>
+            <span className="text-muted-foreground">({activeProviders.join("、")})</span>
           )}
         </div>
         <div className="flex items-center gap-1">
@@ -158,12 +157,7 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
         </div>
       </div>
     );
-  }, [
-    proxyStatusEnabled,
-    proxyStatusLoading,
-    proxyStatusError,
-    activeUserStatus,
-  ]);
+  }, [proxyStatusEnabled, proxyStatusLoading, proxyStatusError, activeUserStatus]);
 
   const handleKeyCreated = (result: { generatedKey: string; name: string }) => {
     setOpenAdd(false); // 关闭表单dialog
@@ -177,7 +171,7 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
       setCopied(true);
       setTimeout(() => setCopied(false), 2000);
     } catch (err) {
-      console.error('复制失败:', err);
+      console.error("复制失败:", err);
     }
   };
 
@@ -187,9 +181,8 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
   };
 
   // 权限检查:管理员可以给所有人添加Key,普通用户只能给自己添加Key
-  const canAddKey = currentUser && activeUser && (
-    currentUser.role === 'admin' || currentUser.id === activeUser.id
-  );
+  const canAddKey =
+    currentUser && activeUser && (currentUser.role === "admin" || currentUser.id === activeUser.id);
 
   return (
     <>
@@ -202,7 +195,7 @@ export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
           <div className="mt-1">
             <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
               <div>
-                今日用量 {activeUser ? formatCurrency(totalTodayUsage) : "-"} / {" "}
+                今日用量 {activeUser ? formatCurrency(totalTodayUsage) : "-"} /{" "}
                 {activeUser ? formatCurrency(activeUser.dailyQuota) : "-"}
               </div>
               {proxyStatusContent}

+ 23 - 36
src/app/dashboard/_components/user/key-list.tsx

@@ -9,11 +9,7 @@ import type { User } from "@/types/user";
 import { format } from "timeago.js";
 import { formatCurrency } from "@/lib/utils/currency";
 import Link from "next/link";
-import {
-  Collapsible,
-  CollapsibleContent,
-  CollapsibleTrigger,
-} from "@/components/ui/collapsible";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
 import {
   Table,
   TableBody,
@@ -54,12 +50,12 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
       setCopiedKeyId(key.id);
       setTimeout(() => setCopiedKeyId(null), 2000);
     } catch (err) {
-      console.error('复制失败:', err);
+      console.error("复制失败:", err);
     }
   };
 
   const columns = [
-    TableColumnTypes.text<UserKeyDisplay>('name', '名称', {
+    TableColumnTypes.text<UserKeyDisplay>("name", "名称", {
       render: (value, record) => (
         <div className="space-y-1">
           <div className="truncate font-medium">{value}</div>
@@ -92,7 +88,9 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
                       {record.modelStats.map((stat) => (
                         <TableRow key={stat.model}>
                           <TableCell className="text-xs py-1.5 font-mono">{stat.model}</TableCell>
-                          <TableCell className="text-xs py-1.5 text-right">{stat.callCount}</TableCell>
+                          <TableCell className="text-xs py-1.5 text-right">
+                            {stat.callCount}
+                          </TableCell>
                           <TableCell className="text-xs py-1.5 text-right font-mono">
                             {formatCurrency(stat.totalCost)}
                           </TableCell>
@@ -105,14 +103,12 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
             </Collapsible>
           )}
         </div>
-      )
+      ),
     }),
-    TableColumnTypes.text<UserKeyDisplay>('maskedKey', 'Key', {
+    TableColumnTypes.text<UserKeyDisplay>("maskedKey", "Key", {
       render: (_, record: UserKeyDisplay) => (
         <div className="group inline-flex items-center gap-1">
-          <div className="font-mono truncate">
-            {record.maskedKey || "-"}
-          </div>
+          <div className="font-mono truncate">{record.maskedKey || "-"}</div>
           {record.canCopy && record.fullKey && (
             <Button
               variant="ghost"
@@ -129,29 +125,25 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
             </Button>
           )}
         </div>
-      )
+      ),
     }),
-    TableColumnTypes.text<UserKeyDisplay>('todayCallCount', '今日调用', {
+    TableColumnTypes.text<UserKeyDisplay>("todayCallCount", "今日调用", {
       render: (value) => (
-        <div className="text-sm">
-          {typeof value === 'number' ? value.toLocaleString() : 0} 次
-        </div>
-      )
+        <div className="text-sm">{typeof value === "number" ? value.toLocaleString() : 0} 次</div>
+      ),
     }),
-    TableColumnTypes.number<UserKeyDisplay>('todayUsage', '今日消耗', {
+    TableColumnTypes.number<UserKeyDisplay>("todayUsage", "今日消耗", {
       render: (value) => {
-        const amount = typeof value === 'number' ? value : 0;
+        const amount = typeof value === "number" ? value : 0;
         return formatCurrency(amount);
-      }
+      },
     }),
-    TableColumnTypes.text<UserKeyDisplay>('lastUsedAt', '最后使用', {
+    TableColumnTypes.text<UserKeyDisplay>("lastUsedAt", "最后使用", {
       render: (_, record: UserKeyDisplay) => (
         <div className="space-y-0.5">
           {record.lastUsedAt ? (
             <>
-              <div className="text-sm">
-                {format(record.lastUsedAt, 'zh_CN')}
-              </div>
+              <div className="text-sm">{format(record.lastUsedAt, "zh_CN")}</div>
               {record.lastProviderName && (
                 <div className="text-xs text-muted-foreground">
                   供应商: {record.lastProviderName}
@@ -162,17 +154,12 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
             <div className="text-sm text-muted-foreground">未使用</div>
           )}
         </div>
-      )
+      ),
     }),
-    TableColumnTypes.actions<UserKeyDisplay>('操作', (value, record) => (
+    TableColumnTypes.actions<UserKeyDisplay>("操作", (value, record) => (
       <div className="flex items-center gap-1">
         <Link href={`/dashboard/logs?keyId=${record.id}`}>
-          <Button
-            variant="ghost"
-            size="sm"
-            className="h-7 text-xs"
-            title="查看详细日志"
-          >
+          <Button variant="ghost" size="sm" className="h-7 text-xs" title="查看详细日志">
             <ExternalLink className="h-3.5 w-3.5 mr-1" />
             日志
           </Button>
@@ -184,7 +171,7 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
           canDelete={canDeleteKeys}
         />
       </div>
-    ))
+    )),
   ];
 
   return (
@@ -193,7 +180,7 @@ export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
       data={keys}
       emptyState={{
         title: "暂无 Key",
-        description: '可点击右上角 "新增 Key" 按钮添加密钥'
+        description: '可点击右上角 "新增 Key" 按钮添加密钥',
       }}
       maxHeight="600px"
       stickyHeader

+ 3 - 3
src/app/dashboard/_components/user/user-key-manager.tsx

@@ -14,9 +14,9 @@ interface UserKeyManagerProps {
 export function UserKeyManager({ users, currentUser }: UserKeyManagerProps) {
   // 普通用户默认选择自己,管理员选择第一个用户
   const getInitialUser = () => {
-    if (currentUser?.role === 'user') {
+    if (currentUser?.role === "user") {
       // 普通用户只能看到自己
-      return users.find(u => u.id === currentUser.id) || users[0];
+      return users.find((u) => u.id === currentUser.id) || users[0];
     }
     // 管理员看到第一个用户
     return users[0];
@@ -26,7 +26,7 @@ export function UserKeyManager({ users, currentUser }: UserKeyManagerProps) {
   const activeUser = users.find((u) => u.id === activeUserId) ?? getInitialUser();
 
   // 普通用户只显示Key列表,不显示用户列表
-  if (currentUser?.role === 'user') {
+  if (currentUser?.role === "user") {
     return (
       <div className="space-y-3">
         <div className="bg-card text-card-foreground border border-border rounded-xl p-4">

+ 3 - 12
src/app/dashboard/_components/user/user-list.tsx

@@ -11,12 +11,7 @@ interface UserListProps {
   currentUser?: User;
 }
 
-export function UserList({
-  users,
-  activeUserId,
-  onUserSelect,
-  currentUser,
-}: UserListProps) {
+export function UserList({ users, activeUserId, onUserSelect, currentUser }: UserListProps) {
   // 转换数据格式
   const listItems: ListItemData[] = users.map((user) => ({
     id: user.id,
@@ -29,9 +24,7 @@ export function UserList({
     metadata: [
       {
         label: "活跃密钥",
-        value: user.keys
-          .filter((k) => k.status === "enabled")
-          .length.toString(),
+        value: user.keys.filter((k) => k.status === "enabled").length.toString(),
       },
       {
         label: "总密钥",
@@ -62,9 +55,7 @@ export function UserList({
       </ListContainer>
 
       {/* 新增用户按钮:列表下方、与列表同宽,中性配色 - 仅管理员可见 */}
-      {currentUser?.role === "admin" && (
-        <AddUserDialog variant="secondary" className="w-full" />
-      )}
+      {currentUser?.role === "admin" && <AddUserDialog variant="secondary" className="w-full" />}
     </div>
   );
 }

+ 1 - 3
src/app/dashboard/layout.tsx

@@ -14,9 +14,7 @@ export default async function DashboardLayout({ children }: { children: ReactNod
   return (
     <div className="min-h-screen bg-background">
       <DashboardHeader session={session} />
-      <main className="mx-auto w-full max-w-7xl px-6 py-8">
-        {children}
-      </main>
+      <main className="mx-auto w-full max-w-7xl px-6 py-8">{children}</main>
     </div>
   );
 }

+ 2 - 9
src/app/dashboard/leaderboard/_components/leaderboard-table.tsx

@@ -62,11 +62,7 @@ export function LeaderboardTable({ data, period }: LeaderboardTableProps) {
       );
     }
 
-    return (
-      <div className="text-muted-foreground font-medium">
-        #{rank}
-      </div>
-    );
+    return <div className="text-muted-foreground font-medium">#{rank}</div>;
   };
 
   return (
@@ -89,10 +85,7 @@ export function LeaderboardTable({ data, period }: LeaderboardTableProps) {
                 const isTopThree = rank <= 3;
 
                 return (
-                  <TableRow
-                    key={entry.userId}
-                    className={isTopThree ? "bg-muted/50" : ""}
-                  >
+                  <TableRow key={entry.userId} className={isTopThree ? "bg-muted/50" : ""}>
                     <TableCell>{getRankBadge(rank)}</TableCell>
                     <TableCell className={isTopThree ? "font-semibold" : ""}>
                       {entry.userName}

+ 1 - 4
src/app/dashboard/leaderboard/_components/leaderboard-view.tsx

@@ -25,10 +25,7 @@ export function LeaderboardView() {
           throw new Error("获取排行榜数据失败");
         }
 
-        const [daily, monthly] = await Promise.all([
-          dailyRes.json(),
-          monthlyRes.json(),
-        ]);
+        const [daily, monthly] = await Promise.all([dailyRes.json(), monthlyRes.json()]);
 
         setDailyData(daily);
         setMonthlyData(monthly);

+ 1 - 4
src/app/dashboard/leaderboard/page.tsx

@@ -6,10 +6,7 @@ export const dynamic = "force-dynamic";
 export default async function LeaderboardPage() {
   return (
     <div className="space-y-6">
-      <Section
-        title="消耗排行榜"
-        description="查看用户消耗排名,数据每 5 分钟更新一次"
-      >
+      <Section title="消耗排行榜" description="查看用户消耗排名,数据每 5 分钟更新一次">
         <LeaderboardView />
       </Section>
     </div>

+ 5 - 10
src/app/dashboard/page.tsx

@@ -1,5 +1,5 @@
-import { redirect } from 'next/navigation';
-import { getSession } from '@/lib/auth';
+import { redirect } from "next/navigation";
+import { getSession } from "@/lib/auth";
 import { Section } from "@/components/section";
 import { UserKeyManager } from "./_components/user/user-key-manager";
 import { getUsers } from "@/actions/users";
@@ -16,7 +16,7 @@ export default async function DashboardPage() {
   // 检查价格表是否存在,如果不存在则跳转到价格上传页面
   const hasPrices = await hasPriceTable();
   if (!hasPrices) {
-    redirect('/settings/prices?required=true');
+    redirect("/settings/prices?required=true");
   }
 
   const [users, session, statistics] = await Promise.all([
@@ -32,15 +32,10 @@ export default async function DashboardPage() {
       </div>
 
       <div>
-        <StatisticsWrapper
-          initialData={statistics.ok ? statistics.data : undefined}
-        />
+        <StatisticsWrapper initialData={statistics.ok ? statistics.data : undefined} />
       </div>
 
-      <Section
-        title="客户端"
-        description="用户和密钥管理"
-      >
+      <Section title="客户端" description="用户和密钥管理">
         <ListErrorBoundary>
           <UserKeyManager users={users} currentUser={session?.user} />
         </ListErrorBoundary>

+ 13 - 34
src/app/dashboard/sessions/[sessionId]/messages/page.tsx

@@ -32,10 +32,10 @@ export default function SessionMessagesPage() {
         if (result.ok) {
           setMessages(result.data);
         } else {
-          setError(result.error || '获取失败');
+          setError(result.error || "获取失败");
         }
       } catch (err) {
-        setError(err instanceof Error ? err.message : '未知错误');
+        setError(err instanceof Error ? err.message : "未知错误");
       } finally {
         setIsLoading(false);
       }
@@ -52,7 +52,7 @@ export default function SessionMessagesPage() {
       setCopied(true);
       setTimeout(() => setCopied(false), 2000);
     } catch (err) {
-      console.error('复制失败:', err);
+      console.error("复制失败:", err);
     }
   };
 
@@ -60,9 +60,9 @@ export default function SessionMessagesPage() {
     if (!messages) return;
 
     const jsonStr = JSON.stringify(messages, null, 2);
-    const blob = new Blob([jsonStr], { type: 'application/json' });
+    const blob = new Blob([jsonStr], { type: "application/json" });
     const url = URL.createObjectURL(blob);
-    const a = document.createElement('a');
+    const a = document.createElement("a");
     a.href = url;
     a.download = `session-${sessionId.substring(0, 8)}-messages.json`;
     document.body.appendChild(a);
@@ -76,31 +76,20 @@ export default function SessionMessagesPage() {
       {/* 标题栏 */}
       <div className="flex items-center justify-between">
         <div className="flex items-center gap-4">
-          <Button
-            variant="outline"
-            size="sm"
-            onClick={() => router.back()}
-          >
+          <Button variant="outline" size="sm" onClick={() => router.back()}>
             <ArrowLeft className="h-4 w-4 mr-2" />
             返回
           </Button>
           <div>
             <h1 className="text-2xl font-bold">Session Messages</h1>
-            <p className="text-sm text-muted-foreground font-mono mt-1">
-              {sessionId}
-            </p>
+            <p className="text-sm text-muted-foreground font-mono mt-1">{sessionId}</p>
           </div>
         </div>
 
         {/* 操作按钮 */}
         {messages !== null && (
           <div className="flex gap-2">
-            <Button
-              variant="outline"
-              size="sm"
-              onClick={handleCopy}
-              disabled={copied}
-            >
+            <Button variant="outline" size="sm" onClick={handleCopy} disabled={copied}>
               {copied ? (
                 <>
                   <Check className="h-4 w-4 mr-2" />
@@ -113,11 +102,7 @@ export default function SessionMessagesPage() {
                 </>
               )}
             </Button>
-            <Button
-              variant="outline"
-              size="sm"
-              onClick={handleDownload}
-            >
+            <Button variant="outline" size="sm" onClick={handleDownload}>
               <Download className="h-4 w-4 mr-2" />
               一键下载
             </Button>
@@ -129,15 +114,11 @@ export default function SessionMessagesPage() {
       <Section title="详细信息" description="查看完整的 Session Messages 数据">
         <div className="space-y-4">
           {isLoading ? (
-            <div className="text-center py-16 text-muted-foreground">
-              加载中...
-            </div>
+            <div className="text-center py-16 text-muted-foreground">加载中...</div>
           ) : error ? (
             <div className="text-center py-16">
-              <div className="text-destructive text-lg mb-2">
-                {error}
-              </div>
-              {error.includes('未存储') && (
+              <div className="text-destructive text-lg mb-2">{error}</div>
+              {error.includes("未存储") && (
                 <p className="text-sm text-muted-foreground">
                   提示:请设置环境变量 STORE_SESSION_MESSAGES=true 以启用 messages 存储
                 </p>
@@ -150,9 +131,7 @@ export default function SessionMessagesPage() {
               </pre>
             </div>
           ) : (
-            <div className="text-center py-16 text-muted-foreground">
-              暂无数据
-            </div>
+            <div className="text-center py-16 text-muted-foreground">暂无数据</div>
           )}
         </div>
       </Section>

+ 17 - 15
src/app/dashboard/sessions/_components/active-sessions-table.tsx

@@ -35,13 +35,21 @@ function formatDuration(durationMs: number | undefined): string {
   }
 }
 
-function getStatusBadge(status: 'in_progress' | 'completed' | 'error', statusCode?: number) {
-  if (status === 'in_progress') {
-    return <Badge variant="default" className="bg-blue-500">进行中</Badge>;
-  } else if (status === 'error' || (statusCode && statusCode >= 400)) {
+function getStatusBadge(status: "in_progress" | "completed" | "error", statusCode?: number) {
+  if (status === "in_progress") {
+    return (
+      <Badge variant="default" className="bg-blue-500">
+        进行中
+      </Badge>
+    );
+  } else if (status === "error" || (statusCode && statusCode >= 400)) {
     return <Badge variant="destructive">错误</Badge>;
   } else {
-    return <Badge variant="outline" className="text-green-600 border-green-600">完成</Badge>;
+    return (
+      <Badge variant="outline" className="text-green-600 border-green-600">
+        完成
+      </Badge>
+    );
   }
 }
 
@@ -57,14 +65,10 @@ export function ActiveSessionsTable({
     <div className="space-y-4">
       <div className="flex items-center justify-between">
         <div className="text-sm text-muted-foreground">
-          共 {sessions.length} 个{inactive ? '非活跃' : '活跃'} Session
+          共 {sessions.length} 个{inactive ? "非活跃" : "活跃"} Session
           {inactive && <span className="ml-2 text-xs">(不计入并发数)</span>}
         </div>
-        {isLoading && (
-          <div className="text-sm text-muted-foreground animate-pulse">
-            刷新中...
-          </div>
-        )}
+        {isLoading && <div className="text-sm text-muted-foreground animate-pulse">刷新中...</div>}
       </div>
 
       <div
@@ -109,7 +113,7 @@ export function ActiveSessionsTable({
                   <TableCell className="font-mono text-xs">{session.model || "-"}</TableCell>
                   <TableCell>
                     <Badge variant="outline" className="text-xs">
-                      {session.apiType === 'codex' ? 'Codex' : 'Chat'}
+                      {session.apiType === "codex" ? "Codex" : "Chat"}
                     </Badge>
                   </TableCell>
                   <TableCell className="text-right font-mono text-xs">
@@ -124,9 +128,7 @@ export function ActiveSessionsTable({
                   <TableCell className="text-right font-mono text-xs">
                     {session.costUsd ? `$${parseFloat(session.costUsd).toFixed(6)}` : "-"}
                   </TableCell>
-                  <TableCell>
-                    {getStatusBadge(session.status, session.statusCode)}
-                  </TableCell>
+                  <TableCell>{getStatusBadge(session.status, session.statusCode)}</TableCell>
                   <TableCell className="text-center">
                     <Link href={`/dashboard/sessions/${session.sessionId}/messages`}>
                       <Button variant="ghost" size="sm">

+ 17 - 22
src/app/dashboard/sessions/_components/session-messages-dialog.tsx

@@ -34,10 +34,10 @@ export function SessionMessagesDialog({ sessionId }: SessionMessagesDialogProps)
       if (result.ok) {
         setMessages(result.data);
       } else {
-        setError(result.error || '获取失败');
+        setError(result.error || "获取失败");
       }
     } catch (err) {
-      setError(err instanceof Error ? err.message : '未知错误');
+      setError(err instanceof Error ? err.message : "未知错误");
     } finally {
       setIsLoading(false);
     }
@@ -50,13 +50,16 @@ export function SessionMessagesDialog({ sessionId }: SessionMessagesDialogProps)
   };
 
   return (
-    <Dialog open={isOpen} onOpenChange={(open) => {
-      if (open) {
-        void handleOpen();
-      } else {
-        handleClose();
-      }
-    }}>
+    <Dialog
+      open={isOpen}
+      onOpenChange={(open) => {
+        if (open) {
+          void handleOpen();
+        } else {
+          handleClose();
+        }
+      }}
+    >
       <DialogTrigger asChild>
         <Button variant="ghost" size="sm">
           <Eye className="h-4 w-4 mr-1" />
@@ -66,20 +69,16 @@ export function SessionMessagesDialog({ sessionId }: SessionMessagesDialogProps)
       <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
         <DialogHeader>
           <DialogTitle>Session Messages</DialogTitle>
-          <DialogDescription className="font-mono text-xs">
-            {sessionId}
-          </DialogDescription>
+          <DialogDescription className="font-mono text-xs">{sessionId}</DialogDescription>
         </DialogHeader>
 
         <div className="space-y-4">
           {isLoading ? (
-            <div className="text-center py-8 text-muted-foreground">
-              加载中...
-            </div>
+            <div className="text-center py-8 text-muted-foreground">加载中...</div>
           ) : error ? (
             <div className="text-center py-8 text-destructive">
               {error}
-              {error.includes('未存储') && (
+              {error.includes("未存储") && (
                 <p className="text-sm text-muted-foreground mt-2">
                   提示:请设置环境变量 STORE_SESSION_MESSAGES=true 以启用 messages 存储
                 </p>
@@ -87,14 +86,10 @@ export function SessionMessagesDialog({ sessionId }: SessionMessagesDialogProps)
             </div>
           ) : messages ? (
             <div className="rounded-md border bg-muted p-4">
-              <pre className="text-xs overflow-x-auto">
-                {JSON.stringify(messages, null, 2)}
-              </pre>
+              <pre className="text-xs overflow-x-auto">{JSON.stringify(messages, null, 2)}</pre>
             </div>
           ) : (
-            <div className="text-center py-8 text-muted-foreground">
-              暂无数据
-            </div>
+            <div className="text-center py-8 text-muted-foreground">暂无数据</div>
           )}
         </div>
       </DialogContent>

+ 5 - 18
src/app/dashboard/sessions/page.tsx

@@ -18,7 +18,7 @@ async function fetchAllSessions(): Promise<{
 }> {
   const result = await getAllSessions();
   if (!result.ok) {
-    throw new Error(result.error || '获取 session 列表失败');
+    throw new Error(result.error || "获取 session 列表失败");
   }
   return result.data;
 }
@@ -29,11 +29,7 @@ async function fetchAllSessions(): Promise<{
 export default function ActiveSessionsPage() {
   const router = useRouter();
 
-  const {
-    data,
-    isLoading,
-    error,
-  } = useQuery<
+  const { data, isLoading, error } = useQuery<
     { active: ActiveSessionInfo[]; inactive: ActiveSessionInfo[] },
     Error
   >({
@@ -61,27 +57,18 @@ export default function ActiveSessionsPage() {
       </div>
 
       {error ? (
-        <div className="text-center text-destructive py-8">
-          加载失败: {error.message}
-        </div>
+        <div className="text-center text-destructive py-8">加载失败: {error.message}</div>
       ) : (
         <>
           {/* 活跃 Session 区域 */}
           <Section title="活跃 Session(最近 5 分钟)">
-            <ActiveSessionsTable
-              sessions={activeSessions}
-              isLoading={isLoading}
-            />
+            <ActiveSessionsTable sessions={activeSessions} isLoading={isLoading} />
           </Section>
 
           {/* 非活跃 Session 区域 */}
           {inactiveSessions.length > 0 && (
             <Section title="非活跃 Session(超过 5 分钟,仅供查看)">
-              <ActiveSessionsTable
-                sessions={inactiveSessions}
-                isLoading={isLoading}
-                inactive
-              />
+              <ActiveSessionsTable sessions={inactiveSessions} isLoading={isLoading} inactive />
             </Section>
           )}
         </>

+ 0 - 1
src/app/globals.css

@@ -112,7 +112,6 @@
   --sidebar-ring: oklch(0.646 0.222 41.116);
 }
 
-
 @layer base {
   * {
     @apply border-border outline-ring/50;

+ 19 - 19
src/app/login/page.tsx

@@ -1,13 +1,13 @@
-'use client';
+"use client";
 
-import { Suspense, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Alert, AlertDescription } from '@/components/ui/alert';
-import { Key, Loader2 } from 'lucide-react';
+import { Suspense, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Key, Loader2 } from "lucide-react";
 
 export default function LoginPage() {
   return (
@@ -20,28 +20,28 @@ export default function LoginPage() {
 function LoginPageContent() {
   const router = useRouter();
   const searchParams = useSearchParams();
-  const from = searchParams.get('from') || '/dashboard';
+  const from = searchParams.get("from") || "/dashboard";
 
-  const [apiKey, setApiKey] = useState('');
+  const [apiKey, setApiKey] = useState("");
   const [loading, setLoading] = useState(false);
-  const [error, setError] = useState('');
+  const [error, setError] = useState("");
 
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
-    setError('');
+    setError("");
     setLoading(true);
 
     try {
-      const response = await fetch('/api/auth/login', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
+      const response = await fetch("/api/auth/login", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ key: apiKey }),
       });
 
       const data = await response.json();
 
       if (!response.ok) {
-        setError(data.error || '登录失败');
+        setError(data.error || "登录失败");
         return;
       }
 
@@ -49,7 +49,7 @@ function LoginPageContent() {
       router.push(from);
       router.refresh();
     } catch {
-      setError('网络错误,请稍后重试');
+      setError("网络错误,请稍后重试");
     } finally {
       setLoading(false);
     }
@@ -110,7 +110,7 @@ function LoginPageContent() {
                       登录中...
                     </>
                   ) : (
-                    '进入控制台'
+                    "进入控制台"
                   )}
                 </Button>
                 <p className="text-center text-xs text-muted-foreground">

+ 1 - 3
src/app/settings/_components/settings-page-header.tsx

@@ -7,9 +7,7 @@ export function SettingsPageHeader({ title, description }: SettingsPageHeaderPro
   return (
     <div className="space-y-1">
       <h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
-      {description ? (
-        <p className="text-sm text-muted-foreground">{description}</p>
-      ) : null}
+      {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
     </div>
   );
 }

+ 1 - 0
src/app/settings/_lib/nav-items.ts

@@ -7,4 +7,5 @@ export const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
   { href: "/settings/config", label: "配置" },
   { href: "/settings/prices", label: "价格表" },
   { href: "/settings/providers", label: "供应商" },
+  { href: "/settings/logs", label: "日志" },
 ];

+ 6 - 2
src/app/settings/config/_components/system-settings-form.tsx

@@ -15,7 +15,9 @@ interface SystemSettingsFormProps {
 
 export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) {
   const [siteTitle, setSiteTitle] = useState(initialSettings.siteTitle);
-  const [allowGlobalUsageView, setAllowGlobalUsageView] = useState(initialSettings.allowGlobalUsageView);
+  const [allowGlobalUsageView, setAllowGlobalUsageView] = useState(
+    initialSettings.allowGlobalUsageView
+  );
   const [isPending, startTransition] = useTransition();
 
   const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -59,7 +61,9 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
           maxLength={128}
           required
         />
-        <p className="text-xs text-muted-foreground">用于设置浏览器标签页标题以及系统默认显示名称。</p>
+        <p className="text-xs text-muted-foreground">
+          用于设置浏览器标签页标题以及系统默认显示名称。
+        </p>
       </div>
 
       <div className="flex items-start justify-between gap-4 rounded-lg border border-dashed border-border px-4 py-3">

+ 1 - 4
src/app/settings/config/page.tsx

@@ -15,10 +15,7 @@ export default async function SettingsConfigPage() {
         description="管理系统的基础参数,影响站点显示和统计行为。"
       />
 
-      <Section
-        title="站点参数"
-        description="配置站点标题与仪表盘统计展示策略。"
-      >
+      <Section title="站点参数" description="配置站点标题与仪表盘统计展示策略。">
         <SystemSettingsForm
           initialSettings={{
             siteTitle: settings.siteTitle,

+ 1 - 3
src/app/settings/layout.tsx

@@ -26,9 +26,7 @@ export default async function SettingsLayout({ children }: { children: ReactNode
             <aside className="lg:sticky lg:top-24 lg:self-start">
               <SettingsNav items={SETTINGS_NAV_ITEMS} />
             </aside>
-            <div className="space-y-6">
-              {children}
-            </div>
+            <div className="space-y-6">{children}</div>
           </div>
         </div>
       </main>

+ 1 - 6
src/app/settings/prices/_components/sync-litellm-button.tsx

@@ -58,12 +58,7 @@ export function SyncLiteLLMButton() {
   };
 
   return (
-    <Button
-      variant="outline"
-      size="sm"
-      onClick={handleSync}
-      disabled={syncing}
-    >
+    <Button variant="outline" size="sm" onClick={handleSync} disabled={syncing}>
       <RefreshCw className={`h-4 w-4 mr-2 ${syncing ? "animate-spin" : ""}`} />
       {syncing ? "同步中..." : "同步 LiteLLM 价格"}
     </Button>

+ 104 - 108
src/app/settings/prices/_components/upload-price-dialog.tsx

@@ -53,7 +53,7 @@ function PageLoadingOverlay({ active }: PageLoadingOverlayProps) {
  */
 export function UploadPriceDialog({
   defaultOpen = false,
-  isRequired = false
+  isRequired = false,
 }: UploadPriceDialogProps) {
   const router = useRouter();
   const [open, setOpen] = useState(defaultOpen);
@@ -133,7 +133,7 @@ export function UploadPriceDialog({
 
     // 如果是必需上传且已成功上传,跳转到dashboard
     if (isRequired && result && (result.added.length > 0 || result.updated.length > 0)) {
-      router.push('/dashboard');
+      router.push("/dashboard");
       return;
     }
 
@@ -164,126 +164,122 @@ export function UploadPriceDialog({
             }
           }}
         >
-        <DialogHeader>
-          <DialogTitle>
-            {isRequired ? "请务必先上传价格表" : "上传模型价格表"}
-          </DialogTitle>
-          <DialogDescription>
-            {isRequired
-              ? "系统检测到尚未配置模型价格,请选择包含模型价格数据的JSON文件进行上传"
-              : "选择包含模型价格数据的JSON文件进行上传"
-            }
-          </DialogDescription>
-        </DialogHeader>
+          <DialogHeader>
+            <DialogTitle>{isRequired ? "请务必先上传价格表" : "上传模型价格表"}</DialogTitle>
+            <DialogDescription>
+              {isRequired
+                ? "系统检测到尚未配置模型价格,请选择包含模型价格数据的JSON文件进行上传"
+                : "选择包含模型价格数据的JSON文件进行上传"}
+            </DialogDescription>
+          </DialogHeader>
 
-        {!result ? (
-          <div className="space-y-4">
-            <div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-6">
-              <div className="flex flex-col items-center space-y-3">
-                <FileJson className="h-10 w-10 text-muted-foreground/50" />
-                <div className="text-center">
-                  <p className="text-sm text-muted-foreground">
-                    点击选择JSON文件或拖拽到此处
-                  </p>
-                  <p className="text-xs text-muted-foreground mt-1">
-                    文件大小不超过10MB
-                  </p>
-                </div>
-                <label htmlFor="price-file-input">
-                  <Button
-                    variant="secondary"
-                    size="sm"
+          {!result ? (
+            <div className="space-y-4">
+              <div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-6">
+                <div className="flex flex-col items-center space-y-3">
+                  <FileJson className="h-10 w-10 text-muted-foreground/50" />
+                  <div className="text-center">
+                    <p className="text-sm text-muted-foreground">点击选择JSON文件或拖拽到此处</p>
+                    <p className="text-xs text-muted-foreground mt-1">文件大小不超过10MB</p>
+                  </div>
+                  <label htmlFor="price-file-input">
+                    <Button variant="secondary" size="sm" disabled={uploading} asChild>
+                      <span>{uploading ? "上传中..." : "选择文件"}</span>
+                    </Button>
+                  </label>
+                  <input
+                    id="price-file-input"
+                    type="file"
+                    accept=".json"
+                    className="hidden"
+                    onChange={handleFileSelect}
                     disabled={uploading}
-                    asChild
-                  >
-                    <span>
-                      {uploading ? "上传中..." : "选择文件"}
-                    </span>
-                  </Button>
-                </label>
-                <input
-                  id="price-file-input"
-                  type="file"
-                  accept=".json"
-                  className="hidden"
-                  onChange={handleFileSelect}
-                  disabled={uploading}
-                />
+                  />
+                </div>
               </div>
-            </div>
 
-            <div className="text-xs text-muted-foreground space-y-1">
-              <p>• 推荐使用左侧&quot;同步 LiteLLM 价格&quot;按钮自动获取最新价格</p>
-              <p>• 也可以手动下载 <a className="text-blue-500 underline" href="https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" target="_blank" rel="noopener noreferrer">LiteLLM 价格表</a> 并上传</p>
-              <p>• 支持 Claude 和 OpenAI 模型(claude-, gpt-, o1-, o3- 前缀)</p>
-            </div>
-          </div>
-        ) : (
-          <div className="space-y-4">
-            <div className="text-sm space-y-2">
-              <div className="flex items-center justify-between p-2 bg-muted/50 rounded">
-                <span>处理总数</span>
-                <span className="font-mono">{result.total}</span>
+              <div className="text-xs text-muted-foreground space-y-1">
+                <p>• 推荐使用左侧&quot;同步 LiteLLM 价格&quot;按钮自动获取最新价格</p>
+                <p>
+                  • 也可以手动下载{" "}
+                  <a
+                    className="text-blue-500 underline"
+                    href="https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                  >
+                    LiteLLM 价格表
+                  </a>{" "}
+                  并上传
+                </p>
+                <p>• 支持 Claude 和 OpenAI 模型(claude-, gpt-, o1-, o3- 前缀)</p>
               </div>
-
-              {result.added.length > 0 && (
-                <div className="p-2 bg-green-50 dark:bg-green-950/20 rounded">
-                  <div className="flex items-center gap-2 mb-1">
-                    <CheckCircle className="h-4 w-4 text-green-600" />
-                    <span className="font-medium">新增模型 ({result.added.length})</span>
-                  </div>
-                  <div className="text-xs text-muted-foreground ml-6">
-                    {result.added.slice(0, 3).join(", ")}
-                    {result.added.length > 3 && ` 等${result.added.length}个`}
-                  </div>
+            </div>
+          ) : (
+            <div className="space-y-4">
+              <div className="text-sm space-y-2">
+                <div className="flex items-center justify-between p-2 bg-muted/50 rounded">
+                  <span>处理总数</span>
+                  <span className="font-mono">{result.total}</span>
                 </div>
-              )}
 
-              {result.updated.length > 0 && (
-                <div className="p-2 bg-blue-50 dark:bg-blue-950/20 rounded">
-                  <div className="flex items-center gap-2 mb-1">
-                    <AlertCircle className="h-4 w-4 text-blue-600" />
-                    <span className="font-medium">更新模型 ({result.updated.length})</span>
-                  </div>
-                  <div className="text-xs text-muted-foreground ml-6">
-                    {result.updated.slice(0, 3).join(", ")}
-                    {result.updated.length > 3 && ` 等${result.updated.length}个`}
+                {result.added.length > 0 && (
+                  <div className="p-2 bg-green-50 dark:bg-green-950/20 rounded">
+                    <div className="flex items-center gap-2 mb-1">
+                      <CheckCircle className="h-4 w-4 text-green-600" />
+                      <span className="font-medium">新增模型 ({result.added.length})</span>
+                    </div>
+                    <div className="text-xs text-muted-foreground ml-6">
+                      {result.added.slice(0, 3).join(", ")}
+                      {result.added.length > 3 && ` 等${result.added.length}个`}
+                    </div>
                   </div>
-                </div>
-              )}
+                )}
 
-              {result.unchanged.length > 0 && (
-                <div className="p-2 bg-gray-50 dark:bg-gray-950/20 rounded">
-                  <div className="flex items-center gap-2">
-                    <span className="font-medium">未变化 ({result.unchanged.length})</span>
+                {result.updated.length > 0 && (
+                  <div className="p-2 bg-blue-50 dark:bg-blue-950/20 rounded">
+                    <div className="flex items-center gap-2 mb-1">
+                      <AlertCircle className="h-4 w-4 text-blue-600" />
+                      <span className="font-medium">更新模型 ({result.updated.length})</span>
+                    </div>
+                    <div className="text-xs text-muted-foreground ml-6">
+                      {result.updated.slice(0, 3).join(", ")}
+                      {result.updated.length > 3 && ` 等${result.updated.length}个`}
+                    </div>
                   </div>
-                </div>
-              )}
+                )}
 
-              {result.failed.length > 0 && (
-                <div className="p-2 bg-red-50 dark:bg-red-950/20 rounded">
-                  <div className="flex items-center gap-2 mb-1">
-                    <XCircle className="h-4 w-4 text-red-600" />
-                    <span className="font-medium">处理失败 ({result.failed.length})</span>
+                {result.unchanged.length > 0 && (
+                  <div className="p-2 bg-gray-50 dark:bg-gray-950/20 rounded">
+                    <div className="flex items-center gap-2">
+                      <span className="font-medium">未变化 ({result.unchanged.length})</span>
+                    </div>
                   </div>
-                  <div className="text-xs text-muted-foreground ml-6">
-                    {result.failed.slice(0, 3).join(", ")}
-                    {result.failed.length > 3 && ` 等${result.failed.length}个`}
+                )}
+
+                {result.failed.length > 0 && (
+                  <div className="p-2 bg-red-50 dark:bg-red-950/20 rounded">
+                    <div className="flex items-center gap-2 mb-1">
+                      <XCircle className="h-4 w-4 text-red-600" />
+                      <span className="font-medium">处理失败 ({result.failed.length})</span>
+                    </div>
+                    <div className="text-xs text-muted-foreground ml-6">
+                      {result.failed.slice(0, 3).join(", ")}
+                      {result.failed.length > 3 && ` 等${result.failed.length}个`}
+                    </div>
                   </div>
-                </div>
-              )}
-            </div>
+                )}
+              </div>
 
-            <Button onClick={handleClose} className="w-full">
-              {isRequired && result && (result.added.length > 0 || result.updated.length > 0)
-                ? "进入控制面板"
-                : "完成"
-              }
-            </Button>
-          </div>
-        )}
-      </DialogContent>
-    </Dialog>
+              <Button onClick={handleClose} className="w-full">
+                {isRequired && result && (result.added.length > 0 || result.updated.length > 0)
+                  ? "进入控制面板"
+                  : "完成"}
+              </Button>
+            </div>
+          )}
+        </DialogContent>
+      </Dialog>
     </>
   );
 }

+ 4 - 9
src/app/settings/prices/page.tsx

@@ -11,20 +11,15 @@ interface SettingsPricesPageProps {
   searchParams: Promise<{ required?: string }>;
 }
 
-export default async function SettingsPricesPage({
-  searchParams
-}: SettingsPricesPageProps) {
+export default async function SettingsPricesPage({ searchParams }: SettingsPricesPageProps) {
   const params = await searchParams;
   const prices = await getModelPrices();
-  const isRequired = params.required === 'true';
+  const isRequired = params.required === "true";
   const isEmpty = prices.length === 0;
 
   return (
     <>
-      <SettingsPageHeader
-        title="价格表"
-        description="管理平台基础配置与模型价格"
-      />
+      <SettingsPageHeader title="价格表" description="管理平台基础配置与模型价格" />
 
       <Section
         title="模型价格"
@@ -40,4 +35,4 @@ export default async function SettingsPricesPage({
       </Section>
     </>
   );
-}
+}

+ 52 - 32
src/app/settings/providers/_components/forms/provider-form.tsx

@@ -3,7 +3,13 @@ import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
 import { DialogHeader, DialogTitle } from "@/components/ui/dialog";
 import { useState, useTransition } from "react";
 import { addProvider, editProvider, removeProvider } from "@/actions/providers";
@@ -34,23 +40,33 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
   const isEdit = mode === "edit";
   const [isPending, startTransition] = useTransition();
 
-  const [name, setName] = useState(isEdit ? provider?.name ?? "" : "");
-  const [url, setUrl] = useState(isEdit ? provider?.url ?? "" : "");
+  const [name, setName] = useState(isEdit ? (provider?.name ?? "") : "");
+  const [url, setUrl] = useState(isEdit ? (provider?.url ?? "") : "");
   const [key, setKey] = useState(""); // 编辑时留空代表不更新
-  const [providerType, setProviderType] = useState<string>(isEdit ? provider?.providerType ?? "claude" : "claude");
+  const [providerType, setProviderType] = useState<string>(
+    isEdit ? (provider?.providerType ?? "claude") : "claude"
+  );
   const [modelRedirects, setModelRedirects] = useState<string>(
-    isEdit && provider?.modelRedirects
-      ? JSON.stringify(provider.modelRedirects, null, 2)
-      : ""
+    isEdit && provider?.modelRedirects ? JSON.stringify(provider.modelRedirects, null, 2) : ""
+  );
+  const [priority, setPriority] = useState<number>(isEdit ? (provider?.priority ?? 0) : 0);
+  const [weight, setWeight] = useState<number>(isEdit ? (provider?.weight ?? 1) : 1);
+  const [costMultiplier, setCostMultiplier] = useState<number>(
+    isEdit ? (provider?.costMultiplier ?? 1.0) : 1.0
+  );
+  const [groupTag, setGroupTag] = useState<string>(isEdit ? (provider?.groupTag ?? "") : "");
+  const [limit5hUsd, setLimit5hUsd] = useState<number | null>(
+    isEdit ? (provider?.limit5hUsd ?? null) : null
+  );
+  const [limitWeeklyUsd, setLimitWeeklyUsd] = useState<number | null>(
+    isEdit ? (provider?.limitWeeklyUsd ?? null) : null
+  );
+  const [limitMonthlyUsd, setLimitMonthlyUsd] = useState<number | null>(
+    isEdit ? (provider?.limitMonthlyUsd ?? null) : null
+  );
+  const [limitConcurrentSessions, setLimitConcurrentSessions] = useState<number | null>(
+    isEdit ? (provider?.limitConcurrentSessions ?? null) : null
   );
-  const [priority, setPriority] = useState<number>(isEdit ? provider?.priority ?? 0 : 0);
-  const [weight, setWeight] = useState<number>(isEdit ? provider?.weight ?? 1 : 1);
-  const [costMultiplier, setCostMultiplier] = useState<number>(isEdit ? provider?.costMultiplier ?? 1.0 : 1.0);
-  const [groupTag, setGroupTag] = useState<string>(isEdit ? provider?.groupTag ?? "" : "");
-  const [limit5hUsd, setLimit5hUsd] = useState<number | null>(isEdit ? provider?.limit5hUsd ?? null : null);
-  const [limitWeeklyUsd, setLimitWeeklyUsd] = useState<number | null>(isEdit ? provider?.limitWeeklyUsd ?? null : null);
-  const [limitMonthlyUsd, setLimitMonthlyUsd] = useState<number | null>(isEdit ? provider?.limitMonthlyUsd ?? null : null);
-  const [limitConcurrentSessions, setLimitConcurrentSessions] = useState<number | null>(isEdit ? provider?.limitConcurrentSessions ?? null : null);
 
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
@@ -69,7 +85,7 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
     if (modelRedirects.trim()) {
       try {
         parsedModelRedirects = JSON.parse(modelRedirects);
-        if (typeof parsedModelRedirects !== 'object' || parsedModelRedirects === null) {
+        if (typeof parsedModelRedirects !== "object" || parsedModelRedirects === null) {
           toast.error("模型重定向必须是一个有效的 JSON 对象");
           return;
         }
@@ -123,7 +139,7 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
           }
           const res = await editProvider(provider.id, updateData);
           if (!res.ok) {
-            toast.error(res.error || '更新服务商失败');
+            toast.error(res.error || "更新服务商失败");
             return;
           }
         } else {
@@ -149,12 +165,12 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
             cc: null,
           });
           if (!res.ok) {
-            toast.error(res.error || '添加服务商失败');
+            toast.error(res.error || "添加服务商失败");
             return;
           }
           // 添加成功提示
-          toast.success('添加服务商成功', {
-            description: `服务商 "${name.trim()}" 已添加`
+          toast.success("添加服务商成功", {
+            description: `服务商 "${name.trim()}" 已添加`,
           });
           // 重置表单(仅新增)
           setName("");
@@ -174,7 +190,7 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
         onSuccess?.();
       } catch (error) {
         console.error(isEdit ? "更新服务商失败:" : "添加服务商失败:", error);
-        toast.error(isEdit ? '更新服务商失败' : '添加服务商失败');
+        toast.error(isEdit ? "更新服务商失败" : "添加服务商失败");
       }
     });
   };
@@ -213,7 +229,9 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
         </div>
 
         <div className="space-y-2">
-          <Label htmlFor={isEdit ? "edit-key" : "key"}>API 密钥{isEdit ? "(留空不更改)" : " *"}</Label>
+          <Label htmlFor={isEdit ? "edit-key" : "key"}>
+            API 密钥{isEdit ? "(留空不更改)" : " *"}
+          </Label>
           <Input
             id={isEdit ? "edit-key" : "key"}
             type="password"
@@ -237,11 +255,7 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
               供应商类型
               <span className="text-xs text-muted-foreground ml-1">(决定调度策略)</span>
             </Label>
-            <Select
-              value={providerType}
-              onValueChange={setProviderType}
-              disabled={isPending}
-            >
+            <Select value={providerType} onValueChange={setProviderType} disabled={isPending}>
               <SelectTrigger id={isEdit ? "edit-provider-type" : "provider-type"}>
                 <SelectValue placeholder="选择供应商类型" />
               </SelectTrigger>
@@ -364,7 +378,9 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
               />
             </div>
             <div className="space-y-2">
-              <Label htmlFor={isEdit ? "edit-limit-weekly" : "limit-weekly"}>周消费上限 (USD)</Label>
+              <Label htmlFor={isEdit ? "edit-limit-weekly" : "limit-weekly"}>
+                周消费上限 (USD)
+              </Label>
               <Input
                 id={isEdit ? "edit-limit-weekly" : "limit-weekly"}
                 type="number"
@@ -380,7 +396,9 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
 
           <div className="grid grid-cols-2 gap-4">
             <div className="space-y-2">
-              <Label htmlFor={isEdit ? "edit-limit-monthly" : "limit-monthly"}>月消费上限 (USD)</Label>
+              <Label htmlFor={isEdit ? "edit-limit-monthly" : "limit-monthly"}>
+                月消费上限 (USD)
+              </Label>
               <Input
                 id={isEdit ? "edit-limit-monthly" : "limit-monthly"}
                 type="number"
@@ -393,7 +411,9 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
               />
             </div>
             <div className="space-y-2">
-              <Label htmlFor={isEdit ? "edit-limit-concurrent" : "limit-concurrent"}>并发 Session 上限</Label>
+              <Label htmlFor={isEdit ? "edit-limit-concurrent" : "limit-concurrent"}>
+                并发 Session 上限
+              </Label>
               <Input
                 id={isEdit ? "edit-limit-concurrent" : "limit-concurrent"}
                 type="number"
@@ -432,13 +452,13 @@ export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
                         try {
                           const res = await removeProvider(provider.id);
                           if (!res.ok) {
-                            toast.error(res.error || '删除服务商失败');
+                            toast.error(res.error || "删除服务商失败");
                             return;
                           }
                           onSuccess?.();
                         } catch (e) {
                           console.error("删除服务商失败", e);
-                          toast.error('删除服务商失败');
+                          toast.error("删除服务商失败");
                         }
                       });
                     }}

+ 85 - 54
src/app/settings/providers/_components/hooks/use-provider-edit.ts

@@ -1,4 +1,5 @@
 import { useRef, useState } from "react";
+import { logger } from '@/lib/logger';
 import { toast } from "sonner";
 import { editProvider } from "@/actions/providers";
 import type { ProviderDisplay } from "@/types/provider";
@@ -25,7 +26,9 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
 
   // 周消费上限
   const [showWeeklyLimit, setShowWeeklyLimit] = useState(false);
-  const [limitWeeklyInfinite, setLimitWeeklyInfinite] = useState<boolean>(item.limitWeeklyUsd === null);
+  const [limitWeeklyInfinite, setLimitWeeklyInfinite] = useState<boolean>(
+    item.limitWeeklyUsd === null
+  );
   const [limitWeeklyValue, setLimitWeeklyValue] = useState<number>(() => {
     return item.limitWeeklyUsd ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN;
   });
@@ -33,7 +36,9 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
 
   // 月消费上限
   const [showMonthlyLimit, setShowMonthlyLimit] = useState(false);
-  const [limitMonthlyInfinite, setLimitMonthlyInfinite] = useState<boolean>(item.limitMonthlyUsd === null);
+  const [limitMonthlyInfinite, setLimitMonthlyInfinite] = useState<boolean>(
+    item.limitMonthlyUsd === null
+  );
   const [limitMonthlyValue, setLimitMonthlyValue] = useState<number>(() => {
     return item.limitMonthlyUsd ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN;
   });
@@ -41,9 +46,13 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
 
   // 并发Session上限
   const [showConcurrent, setShowConcurrent] = useState(false);
-  const [concurrentInfinite, setConcurrentInfinite] = useState<boolean>(item.limitConcurrentSessions === 0);
+  const [concurrentInfinite, setConcurrentInfinite] = useState<boolean>(
+    item.limitConcurrentSessions === 0
+  );
   const [concurrentValue, setConcurrentValue] = useState<number>(() => {
-    return item.limitConcurrentSessions === 0 ? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN : item.limitConcurrentSessions;
+    return item.limitConcurrentSessions === 0
+      ? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN
+      : item.limitConcurrentSessions;
   });
   const initialConcurrentRef = useRef<number>(item.limitConcurrentSessions);
 
@@ -60,9 +69,9 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
         throw new Error(res.error);
       }
     } catch (e) {
-      console.error("切换服务商启用状态失败", e);
+      logger.error('切换服务商启用状态失败', { context: e });
       setEnabled(prev);
-      const msg = e instanceof Error ? e.message : '切换失败';
+      const msg = e instanceof Error ? e.message : "切换失败";
       toast.error(msg);
     } finally {
       setTogglePending(false);
@@ -80,14 +89,16 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
 
     const next = clampWeight(weight);
     if (next !== clampWeight(initialWeightRef.current)) {
-      editProvider(item.id, { weight: next }).then(res => {
-        if (!res.ok) throw new Error(res.error);
-      }).catch((e) => {
-        console.error("更新权重失败", e);
-        const msg = e instanceof Error ? e.message : '更新权重失败';
-        toast.error(msg);
-        setWeight(clampWeight(initialWeightRef.current));
-      });
+      editProvider(item.id, { weight: next })
+        .then((res) => {
+          if (!res.ok) throw new Error(res.error);
+        })
+        .catch((e) => {
+          logger.error('更新权重失败', { context: e });
+          const msg = e instanceof Error ? e.message : "更新权重失败";
+          toast.error(msg);
+          setWeight(clampWeight(initialWeightRef.current));
+        });
     }
   };
 
@@ -100,17 +111,21 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
       return;
     }
 
-    const nextValue = limit5hInfinite ? null : Math.max(PROVIDER_LIMITS.LIMIT_5H_USD.MIN, limit5hValue);
+    const nextValue = limit5hInfinite
+      ? null
+      : Math.max(PROVIDER_LIMITS.LIMIT_5H_USD.MIN, limit5hValue);
     if (nextValue !== initial5hRef.current) {
-      editProvider(item.id, { limit_5h_usd: nextValue }).then(res => {
-        if (!res.ok) throw new Error(res.error);
-      }).catch((e) => {
-        console.error("更新5小时消费上限失败", e);
-        const msg = e instanceof Error ? e.message : '更新5小时消费上限失败';
-        toast.error(msg);
-        setLimit5hInfinite(initial5hRef.current === null);
-        setLimit5hValue(initial5hRef.current ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN);
-      });
+      editProvider(item.id, { limit_5h_usd: nextValue })
+        .then((res) => {
+          if (!res.ok) throw new Error(res.error);
+        })
+        .catch((e) => {
+          logger.error('更新5小时消费上限失败', { context: e });
+          const msg = e instanceof Error ? e.message : "更新5小时消费上限失败";
+          toast.error(msg);
+          setLimit5hInfinite(initial5hRef.current === null);
+          setLimit5hValue(initial5hRef.current ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN);
+        });
     }
   };
 
@@ -123,17 +138,21 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
       return;
     }
 
-    const nextValue = limitWeeklyInfinite ? null : Math.max(PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN, limitWeeklyValue);
+    const nextValue = limitWeeklyInfinite
+      ? null
+      : Math.max(PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN, limitWeeklyValue);
     if (nextValue !== initialWeeklyRef.current) {
-      editProvider(item.id, { limit_weekly_usd: nextValue }).then(res => {
-        if (!res.ok) throw new Error(res.error);
-      }).catch((e) => {
-        console.error("更新周消费上限失败", e);
-        const msg = e instanceof Error ? e.message : '更新周消费上限失败';
-        toast.error(msg);
-        setLimitWeeklyInfinite(initialWeeklyRef.current === null);
-        setLimitWeeklyValue(initialWeeklyRef.current ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN);
-      });
+      editProvider(item.id, { limit_weekly_usd: nextValue })
+        .then((res) => {
+          if (!res.ok) throw new Error(res.error);
+        })
+        .catch((e) => {
+          logger.error('更新周消费上限失败', { context: e });
+          const msg = e instanceof Error ? e.message : "更新周消费上限失败";
+          toast.error(msg);
+          setLimitWeeklyInfinite(initialWeeklyRef.current === null);
+          setLimitWeeklyValue(initialWeeklyRef.current ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN);
+        });
     }
   };
 
@@ -146,17 +165,21 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
       return;
     }
 
-    const nextValue = limitMonthlyInfinite ? null : Math.max(PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN, limitMonthlyValue);
+    const nextValue = limitMonthlyInfinite
+      ? null
+      : Math.max(PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN, limitMonthlyValue);
     if (nextValue !== initialMonthlyRef.current) {
-      editProvider(item.id, { limit_monthly_usd: nextValue }).then(res => {
-        if (!res.ok) throw new Error(res.error);
-      }).catch((e) => {
-        console.error("更新月消费上限失败", e);
-        const msg = e instanceof Error ? e.message : '更新月消费上限失败';
-        toast.error(msg);
-        setLimitMonthlyInfinite(initialMonthlyRef.current === null);
-        setLimitMonthlyValue(initialMonthlyRef.current ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN);
-      });
+      editProvider(item.id, { limit_monthly_usd: nextValue })
+        .then((res) => {
+          if (!res.ok) throw new Error(res.error);
+        })
+        .catch((e) => {
+          logger.error('更新月消费上限失败', { context: e });
+          const msg = e instanceof Error ? e.message : "更新月消费上限失败";
+          toast.error(msg);
+          setLimitMonthlyInfinite(initialMonthlyRef.current === null);
+          setLimitMonthlyValue(initialMonthlyRef.current ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN);
+        });
     }
   };
 
@@ -169,17 +192,25 @@ export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
       return;
     }
 
-    const nextValue = concurrentInfinite ? 0 : Math.max(PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN, concurrentValue);
+    const nextValue = concurrentInfinite
+      ? 0
+      : Math.max(PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN, concurrentValue);
     if (nextValue !== initialConcurrentRef.current) {
-      editProvider(item.id, { limit_concurrent_sessions: nextValue }).then(res => {
-        if (!res.ok) throw new Error(res.error);
-      }).catch((e) => {
-        console.error("更新并发Session上限失败", e);
-        const msg = e instanceof Error ? e.message : '更新并发Session上限失败';
-        toast.error(msg);
-        setConcurrentInfinite(initialConcurrentRef.current === 0);
-        setConcurrentValue(initialConcurrentRef.current === 0 ? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN : initialConcurrentRef.current);
-      });
+      editProvider(item.id, { limit_concurrent_sessions: nextValue })
+        .then((res) => {
+          if (!res.ok) throw new Error(res.error);
+        })
+        .catch((e) => {
+          logger.error('更新并发Session上限失败', { context: e });
+          const msg = e instanceof Error ? e.message : "更新并发Session上限失败";
+          toast.error(msg);
+          setConcurrentInfinite(initialConcurrentRef.current === 0);
+          setConcurrentValue(
+            initialConcurrentRef.current === 0
+              ? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN
+              : initialConcurrentRef.current
+          );
+        });
     }
   };
 

+ 115 - 47
src/app/settings/providers/_components/provider-list-item.tsx

@@ -30,7 +30,7 @@ interface ProviderListItemProps {
   item: ProviderDisplay;
   currentUser?: User;
   healthStatus?: {
-    circuitState: 'closed' | 'open' | 'half-open';
+    circuitState: "closed" | "open" | "half-open";
     failureCount: number;
     lastFailureTime: number | null;
     circuitOpenUntil: number | null;
@@ -41,7 +41,7 @@ interface ProviderListItemProps {
 export function ProviderListItem({ item, currentUser, healthStatus }: ProviderListItemProps) {
   const [openEdit, setOpenEdit] = useState(false);
   const [resetPending, startResetTransition] = useTransition();
-  const canEdit = currentUser?.role === 'admin';
+  const canEdit = currentUser?.role === "admin";
 
   const {
     enabled,
@@ -83,18 +83,18 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
       try {
         const res = await resetProviderCircuit(item.id);
         if (res.ok) {
-          toast.success('熔断器已重置', {
+          toast.success("熔断器已重置", {
             description: `供应商 "${item.name}" 的熔断状态已解除`,
           });
         } else {
-          toast.error('重置熔断器失败', {
-            description: res.error || '未知错误',
+          toast.error("重置熔断器失败", {
+            description: res.error || "未知错误",
           });
         }
       } catch (error) {
-        console.error('重置熔断器失败:', error);
-        toast.error('重置熔断器失败', {
-          description: '操作过程中出现异常',
+        console.error("重置熔断器失败:", error);
+        toast.error("重置熔断器失败", {
+          description: "操作过程中出现异常",
         });
       }
     });
@@ -105,13 +105,17 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
       <div className="flex items-start justify-between gap-3 mb-3">
         <div className="flex-1 min-w-0">
           <div className="flex items-center gap-2 mb-1 flex-wrap">
-            <span className={`inline-flex h-5 w-5 items-center justify-center rounded-md text-[10px] font-semibold ${enabled ? "bg-green-500/15 text-green-600" : "bg-muted text-muted-foreground"}`}>
+            <span
+              className={`inline-flex h-5 w-5 items-center justify-center rounded-md text-[10px] font-semibold ${enabled ? "bg-green-500/15 text-green-600" : "bg-muted text-muted-foreground"}`}
+            >
             </span>
-            <h3 className="text-sm font-semibold text-foreground truncate tracking-tight">{item.name}</h3>
+            <h3 className="text-sm font-semibold text-foreground truncate tracking-tight">
+              {item.name}
+            </h3>
 
             {/* 熔断器状态徽章 */}
-            {healthStatus?.circuitState === 'open' && (
+            {healthStatus?.circuitState === "open" && (
               <>
                 <Badge variant="destructive" className="text-xs h-5 px-2">
                   🔴 熔断中
@@ -134,7 +138,9 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                         disabled={resetPending}
                         title="手动解除熔断"
                       >
-                        <RotateCcw className={`h-3.5 w-3.5 ${resetPending ? 'animate-spin' : ''}`} />
+                        <RotateCcw
+                          className={`h-3.5 w-3.5 ${resetPending ? "animate-spin" : ""}`}
+                        />
                       </Button>
                     </AlertDialogTrigger>
                     <AlertDialogContent>
@@ -143,22 +149,25 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                         <AlertDialogDescription>
                           确定要手动解除供应商 &ldquo;{item.name}&rdquo; 的熔断状态吗?
                           <br />
-                          <span className="text-destructive font-medium">请确保上游服务已恢复正常,否则可能导致请求持续失败。</span>
+                          <span className="text-destructive font-medium">
+                            请确保上游服务已恢复正常,否则可能导致请求持续失败。
+                          </span>
                         </AlertDialogDescription>
                       </AlertDialogHeader>
                       <div className="flex gap-2 justify-end">
                         <AlertDialogCancel>取消</AlertDialogCancel>
-                        <AlertDialogAction onClick={handleResetCircuit}>
-                          确认解除
-                        </AlertDialogAction>
+                        <AlertDialogAction onClick={handleResetCircuit}>确认解除</AlertDialogAction>
                       </div>
                     </AlertDialogContent>
                   </AlertDialog>
                 )}
               </>
             )}
-            {healthStatus?.circuitState === 'half-open' && (
-              <Badge variant="secondary" className="text-xs h-5 px-2 border-yellow-500/50 bg-yellow-500/10 text-yellow-700">
+            {healthStatus?.circuitState === "half-open" && (
+              <Badge
+                variant="secondary"
+                className="text-xs h-5 px-2 border-yellow-500/50 bg-yellow-500/10 text-yellow-700"
+              >
                 🟡 恢复中
               </Badge>
             )}
@@ -179,7 +188,11 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                 </DialogTrigger>
                 <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
                   <FormErrorBoundary>
-                    <ProviderForm mode="edit" provider={item} onSuccess={() => setOpenEdit(false)} />
+                    <ProviderForm
+                      mode="edit"
+                      provider={item}
+                      onSuccess={() => setOpenEdit(false)}
+                    />
                   </FormErrorBoundary>
                 </DialogContent>
               </Dialog>
@@ -204,22 +217,23 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
         <div className="flex items-center gap-2">
           <span className="font-medium text-foreground/80">今日用量:</span>
           <span className="tabular-nums">
-            ${(parseFloat(item.todayTotalCostUsd || '0')).toFixed(2)} ({item.todayCallCount ?? 0} 次调用)
+            ${parseFloat(item.todayTotalCostUsd || "0").toFixed(2)} ({item.todayCallCount ?? 0}{" "}
+            次调用)
           </span>
         </div>
         <div className="flex items-center gap-2">
           <span className="font-medium text-foreground/80">最近调用:</span>
           <span className="tabular-nums">
             {item.lastCallTime
-              ? new Date(item.lastCallTime).toLocaleString('zh-CN', {
-                  year: 'numeric',
-                  month: '2-digit',
-                  day: '2-digit',
-                  hour: '2-digit',
-                  minute: '2-digit'
+              ? new Date(item.lastCallTime).toLocaleString("zh-CN", {
+                  year: "numeric",
+                  month: "2-digit",
+                  day: "2-digit",
+                  hour: "2-digit",
+                  minute: "2-digit",
                 })
-              : '-'}
-            {item.lastCallModel && item.lastCallTime ? ` - ${item.lastCallModel}` : ''}
+              : "-"}
+            {item.lastCallModel && item.lastCallTime ? ` - ${item.lastCallModel}` : ""}
           </span>
         </div>
       </div>
@@ -267,7 +281,13 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                     <span>调整权重</span>
                     <span className="font-medium text-foreground">{weight}</span>
                   </div>
-                  <Slider min={PROVIDER_LIMITS.WEIGHT.MIN} max={PROVIDER_LIMITS.WEIGHT.MAX} step={1} value={[weight]} onValueChange={(v) => setWeight(v?.[0] ?? PROVIDER_LIMITS.WEIGHT.MIN)} />
+                  <Slider
+                    min={PROVIDER_LIMITS.WEIGHT.MIN}
+                    max={PROVIDER_LIMITS.WEIGHT.MAX}
+                    step={1}
+                    value={[weight]}
+                    onValueChange={(v) => setWeight(v?.[0] ?? PROVIDER_LIMITS.WEIGHT.MIN)}
+                  />
                 </PopoverContent>
               </Popover>
             ) : (
@@ -289,7 +309,7 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
           <div className="min-w-0 text-center">
             <div className="text-muted-foreground">分组</div>
             <div className="w-full text-center font-medium truncate text-foreground">
-              <span>{item.groupTag || '-'}</span>
+              <span>{item.groupTag || "-"}</span>
             </div>
           </div>
         </div>
@@ -302,7 +322,10 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
             {canEdit ? (
               <Popover open={show5hLimit} onOpenChange={handle5hLimitPopover}>
                 <PopoverTrigger asChild>
-                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                  <button
+                    type="button"
+                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
+                  >
                     <span>{limit5hInfinite ? "∞" : `$${limit5hValue.toFixed(2)}`}</span>
                   </button>
                 </PopoverTrigger>
@@ -311,7 +334,11 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                     <span className="text-muted-foreground">5小时消费上限 (USD)</span>
                     <div className="flex items-center gap-2 text-muted-foreground">
                       <span>无限</span>
-                      <Switch checked={limit5hInfinite} onCheckedChange={setLimit5hInfinite} aria-label="无限" />
+                      <Switch
+                        checked={limit5hInfinite}
+                        onCheckedChange={setLimit5hInfinite}
+                        aria-label="无限"
+                      />
                     </div>
                   </div>
                   <div className="flex items-center gap-3">
@@ -320,10 +347,15 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                       max={PROVIDER_LIMITS.LIMIT_5H_USD.MAX}
                       step={PROVIDER_LIMITS.LIMIT_5H_USD.STEP}
                       value={[limit5hValue]}
-                      onValueChange={(v) => !limit5hInfinite && setLimit5hValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN)}
+                      onValueChange={(v) =>
+                        !limit5hInfinite &&
+                        setLimit5hValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_5H_USD.MIN)
+                      }
                       disabled={limit5hInfinite}
                     />
-                    <span className="w-16 text-right text-xs font-medium">{limit5hInfinite ? "∞" : `$${limit5hValue.toFixed(2)}`}</span>
+                    <span className="w-16 text-right text-xs font-medium">
+                      {limit5hInfinite ? "∞" : `$${limit5hValue.toFixed(2)}`}
+                    </span>
                   </div>
                 </PopoverContent>
               </Popover>
@@ -340,7 +372,10 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
             {canEdit ? (
               <Popover open={showWeeklyLimit} onOpenChange={handleWeeklyLimitPopover}>
                 <PopoverTrigger asChild>
-                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                  <button
+                    type="button"
+                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
+                  >
                     <span>{limitWeeklyInfinite ? "∞" : `$${limitWeeklyValue.toFixed(2)}`}</span>
                   </button>
                 </PopoverTrigger>
@@ -349,7 +384,11 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                     <span className="text-muted-foreground">周消费上限 (USD)</span>
                     <div className="flex items-center gap-2 text-muted-foreground">
                       <span>无限</span>
-                      <Switch checked={limitWeeklyInfinite} onCheckedChange={setLimitWeeklyInfinite} aria-label="无限" />
+                      <Switch
+                        checked={limitWeeklyInfinite}
+                        onCheckedChange={setLimitWeeklyInfinite}
+                        aria-label="无限"
+                      />
                     </div>
                   </div>
                   <div className="flex items-center gap-3">
@@ -358,10 +397,15 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                       max={PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MAX}
                       step={PROVIDER_LIMITS.LIMIT_WEEKLY_USD.STEP}
                       value={[limitWeeklyValue]}
-                      onValueChange={(v) => !limitWeeklyInfinite && setLimitWeeklyValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN)}
+                      onValueChange={(v) =>
+                        !limitWeeklyInfinite &&
+                        setLimitWeeklyValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_WEEKLY_USD.MIN)
+                      }
                       disabled={limitWeeklyInfinite}
                     />
-                    <span className="w-16 text-right text-xs font-medium">{limitWeeklyInfinite ? "∞" : `$${limitWeeklyValue.toFixed(2)}`}</span>
+                    <span className="w-16 text-right text-xs font-medium">
+                      {limitWeeklyInfinite ? "∞" : `$${limitWeeklyValue.toFixed(2)}`}
+                    </span>
                   </div>
                 </PopoverContent>
               </Popover>
@@ -378,7 +422,10 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
             {canEdit ? (
               <Popover open={showMonthlyLimit} onOpenChange={handleMonthlyLimitPopover}>
                 <PopoverTrigger asChild>
-                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                  <button
+                    type="button"
+                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
+                  >
                     <span>{limitMonthlyInfinite ? "∞" : `$${limitMonthlyValue.toFixed(2)}`}</span>
                   </button>
                 </PopoverTrigger>
@@ -387,7 +434,11 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                     <span className="text-muted-foreground">月消费上限 (USD)</span>
                     <div className="flex items-center gap-2 text-muted-foreground">
                       <span>无限</span>
-                      <Switch checked={limitMonthlyInfinite} onCheckedChange={setLimitMonthlyInfinite} aria-label="无限" />
+                      <Switch
+                        checked={limitMonthlyInfinite}
+                        onCheckedChange={setLimitMonthlyInfinite}
+                        aria-label="无限"
+                      />
                     </div>
                   </div>
                   <div className="flex items-center gap-3">
@@ -396,10 +447,15 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                       max={PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MAX}
                       step={PROVIDER_LIMITS.LIMIT_MONTHLY_USD.STEP}
                       value={[limitMonthlyValue]}
-                      onValueChange={(v) => !limitMonthlyInfinite && setLimitMonthlyValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN)}
+                      onValueChange={(v) =>
+                        !limitMonthlyInfinite &&
+                        setLimitMonthlyValue(v?.[0] ?? PROVIDER_LIMITS.LIMIT_MONTHLY_USD.MIN)
+                      }
                       disabled={limitMonthlyInfinite}
                     />
-                    <span className="w-16 text-right text-xs font-medium">{limitMonthlyInfinite ? "∞" : `$${limitMonthlyValue.toFixed(2)}`}</span>
+                    <span className="w-16 text-right text-xs font-medium">
+                      {limitMonthlyInfinite ? "∞" : `$${limitMonthlyValue.toFixed(2)}`}
+                    </span>
                   </div>
                 </PopoverContent>
               </Popover>
@@ -416,7 +472,10 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
             {canEdit ? (
               <Popover open={showConcurrent} onOpenChange={handleConcurrentPopover}>
                 <PopoverTrigger asChild>
-                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                  <button
+                    type="button"
+                    className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer"
+                  >
                     <span>{concurrentInfinite ? "∞" : concurrentValue.toLocaleString()}</span>
                   </button>
                 </PopoverTrigger>
@@ -425,7 +484,11 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                     <span className="text-muted-foreground">并发Session上限</span>
                     <div className="flex items-center gap-2 text-muted-foreground">
                       <span>无限</span>
-                      <Switch checked={concurrentInfinite} onCheckedChange={setConcurrentInfinite} aria-label="无限" />
+                      <Switch
+                        checked={concurrentInfinite}
+                        onCheckedChange={setConcurrentInfinite}
+                        aria-label="无限"
+                      />
                     </div>
                   </div>
                   <div className="flex items-center gap-3">
@@ -434,10 +497,15 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
                       max={PROVIDER_LIMITS.CONCURRENT_SESSIONS.MAX}
                       step={1}
                       value={[concurrentValue]}
-                      onValueChange={(v) => !concurrentInfinite && setConcurrentValue(v?.[0] ?? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN)}
+                      onValueChange={(v) =>
+                        !concurrentInfinite &&
+                        setConcurrentValue(v?.[0] ?? PROVIDER_LIMITS.CONCURRENT_SESSIONS.MIN)
+                      }
                       disabled={concurrentInfinite}
                     />
-                    <span className="w-16 text-right text-xs font-medium">{concurrentInfinite ? "∞" : concurrentValue.toLocaleString()}</span>
+                    <span className="w-16 text-right text-xs font-medium">
+                      {concurrentInfinite ? "∞" : concurrentValue.toLocaleString()}
+                    </span>
                   </div>
                 </PopoverContent>
               </Popover>
@@ -456,4 +524,4 @@ export function ProviderListItem({ item, currentUser, healthStatus }: ProviderLi
       </div>
     </div>
   );
-}
+}

+ 12 - 11
src/app/settings/providers/_components/provider-list.tsx

@@ -7,13 +7,16 @@ import { ProviderListItem } from "./provider-list-item";
 interface ProviderListProps {
   providers: ProviderDisplay[];
   currentUser?: User;
-  healthStatus: Record<number, {
-    circuitState: 'closed' | 'open' | 'half-open';
-    failureCount: number;
-    lastFailureTime: number | null;
-    circuitOpenUntil: number | null;
-    recoveryMinutes: number | null;
-  }>;
+  healthStatus: Record<
+    number,
+    {
+      circuitState: "closed" | "open" | "half-open";
+      failureCount: number;
+      lastFailureTime: number | null;
+      circuitOpenUntil: number | null;
+      recoveryMinutes: number | null;
+    }
+  >;
 }
 
 export function ProviderList({ providers, currentUser, healthStatus }: ProviderListProps) {
@@ -24,9 +27,7 @@ export function ProviderList({ providers, currentUser, healthStatus }: ProviderL
           <Globe className="h-6 w-6 text-muted-foreground" />
         </div>
         <h3 className="font-medium text-foreground mb-1">暂无服务商配置</h3>
-        <p className="text-sm text-muted-foreground text-center">
-          添加你的第一个 API 服务商
-        </p>
+        <p className="text-sm text-muted-foreground text-center">添加你的第一个 API 服务商</p>
       </div>
     );
   }
@@ -43,4 +44,4 @@ export function ProviderList({ providers, currentUser, healthStatus }: ProviderL
       ))}
     </div>
   );
-}
+}

+ 12 - 13
src/app/settings/providers/_components/provider-manager.tsx

@@ -6,25 +6,24 @@ import type { User } from "@/types/user";
 interface ProviderManagerProps {
   providers: ProviderDisplay[];
   currentUser?: User;
-  healthStatus: Record<number, {
-    circuitState: 'closed' | 'open' | 'half-open';
-    failureCount: number;
-    lastFailureTime: number | null;
-    circuitOpenUntil: number | null;
-    recoveryMinutes: number | null;
-  }>;
+  healthStatus: Record<
+    number,
+    {
+      circuitState: "closed" | "open" | "half-open";
+      failureCount: number;
+      lastFailureTime: number | null;
+      circuitOpenUntil: number | null;
+      recoveryMinutes: number | null;
+    }
+  >;
 }
 
 export function ProviderManager({ providers, currentUser, healthStatus }: ProviderManagerProps) {
   return (
     <div className="space-y-4">
-      <ProviderList
-        providers={providers}
-        currentUser={currentUser}
-        healthStatus={healthStatus}
-      />
+      <ProviderList providers={providers} currentUser={currentUser} healthStatus={healthStatus} />
     </div>
   );
 }
 
-export type { ProviderDisplay } from "@/types/provider";
+export type { ProviderDisplay } from "@/types/provider";

+ 58 - 44
src/app/settings/providers/_components/scheduling-rules-dialog.tsx

@@ -9,11 +9,7 @@ import {
   DialogTrigger,
 } from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
-import {
-  Collapsible,
-  CollapsibleContent,
-  CollapsibleTrigger,
-} from "@/components/ui/collapsible";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Badge } from "@/components/ui/badge";
 import { Info, ChevronDown, ChevronRight, Lightbulb } from "lucide-react";
@@ -46,8 +42,8 @@ const scenarios: Array<{
         example: {
           before: "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)",
           after: "筛选出最高优先级(0)的供应商:A, C",
-          decision: "只从 A 和 C 中选择,B 和 D 被过滤"
-        }
+          decision: "只从 A 和 C 中选择,B 和 D 被过滤",
+        },
       },
       {
         step: "成本排序",
@@ -55,8 +51,8 @@ const scenarios: Array<{
         example: {
           before: "A (成本 1.0x), C (成本 0.8x)",
           after: "排序后:C (0.8x), A (1.0x)",
-          decision: "成本更低的 C 有更高的被选中概率"
-        }
+          decision: "成本更低的 C 有更高的被选中概率",
+        },
       },
       {
         step: "加权随机",
@@ -64,10 +60,10 @@ const scenarios: Array<{
         example: {
           before: "C (权重 3), A (权重 1)",
           after: "C 被选中概率 75%, A 被选中概率 25%",
-          decision: "最终随机选择了 C"
-        }
-      }
-    ]
+          decision: "最终随机选择了 C",
+        },
+      },
+    ],
   },
   {
     title: "用户分组过滤",
@@ -80,8 +76,8 @@ const scenarios: Array<{
         example: {
           before: "所有供应商:A (default), B (premium), C (premium), D (economy)",
           after: "过滤出 'premium' 组:B, C",
-          decision: "只从 B 和 C 中选择"
-        }
+          decision: "只从 B 和 C 中选择",
+        },
       },
       {
         step: "分组降级",
@@ -89,10 +85,10 @@ const scenarios: Array<{
         example: {
           before: "用户组 'vip' 内的供应商全部禁用或超限",
           after: "降级到所有启用的供应商:A, B, C, D",
-          decision: "记录警告并从全局供应商池中选择"
-        }
-      }
-    ]
+          decision: "记录警告并从全局供应商池中选择",
+        },
+      },
+    ],
   },
   {
     title: "健康度过滤(熔断器 + 限流)",
@@ -105,8 +101,8 @@ const scenarios: Array<{
         example: {
           before: "供应商 A 连续失败 5 次,熔断器状态:open",
           after: "A 被过滤,剩余:B, C, D",
-          decision: "A 在 60 秒后自动恢复到半开状态"
-        }
+          decision: "A 在 60 秒后自动恢复到半开状态",
+        },
       },
       {
         step: "金额限流",
@@ -114,8 +110,8 @@ const scenarios: Array<{
         example: {
           before: "供应商 B 的 5 小时限额 $10,已消耗 $9.8",
           after: "B 被过滤(接近限额),剩余:C, D",
-          decision: "5 小时窗口滑动后自动恢复"
-        }
+          decision: "5 小时窗口滑动后自动恢复",
+        },
       },
       {
         step: "并发 Session 限制",
@@ -123,10 +119,10 @@ const scenarios: Array<{
         example: {
           before: "供应商 C 并发限制 2,当前活跃 Session 数:2",
           after: "C 被过滤(已满),剩余:D",
-          decision: "Session 过期(5 分钟)后自动释放"
-        }
-      }
-    ]
+          decision: "Session 过期(5 分钟)后自动释放",
+        },
+      },
+    ],
   },
   {
     title: "会话复用机制",
@@ -139,8 +135,8 @@ const scenarios: Array<{
         example: {
           before: "最近一次请求使用了供应商 B",
           after: "检查 B 是否启用且健康",
-          decision: "B 可用,直接复用,跳过随机选择"
-        }
+          decision: "B 可用,直接复用,跳过随机选择",
+        },
       },
       {
         step: "复用失效",
@@ -148,14 +144,14 @@ const scenarios: Array<{
         example: {
           before: "上次使用的供应商 B 已被禁用或熔断",
           after: "进入正常选择流程",
-          decision: "从其他可用供应商中选择"
-        }
-      }
-    ]
-  }
+          decision: "从其他可用供应商中选择",
+        },
+      },
+    ],
+  },
 ];
 
-function ScenarioCard({ title, emoji, description, steps }: typeof scenarios[0]) {
+function ScenarioCard({ title, emoji, description, steps }: (typeof scenarios)[0]) {
   const [isOpen, setIsOpen] = useState(false);
 
   return (
@@ -234,10 +230,18 @@ export function SchedulingRulesDialog() {
             <Info className="h-4 w-4" />
             <AlertTitle>核心原则</AlertTitle>
             <AlertDescription className="space-y-1 text-sm">
-              <p>1️⃣ <strong>优先级优先</strong>:只从最高优先级(数值最小)的供应商中选择</p>
-              <p>2️⃣ <strong>成本优化</strong>:同优先级内,成本倍率低的供应商有更高概率</p>
-              <p>3️⃣ <strong>健康过滤</strong>:自动跳过熔断或超限的供应商</p>
-              <p>4️⃣ <strong>会话复用</strong>:连续对话复用同一供应商,节省上下文成本</p>
+              <p>
+                1️⃣ <strong>优先级优先</strong>:只从最高优先级(数值最小)的供应商中选择
+              </p>
+              <p>
+                2️⃣ <strong>成本优化</strong>:同优先级内,成本倍率低的供应商有更高概率
+              </p>
+              <p>
+                3️⃣ <strong>健康过滤</strong>:自动跳过熔断或超限的供应商
+              </p>
+              <p>
+                4️⃣ <strong>会话复用</strong>:连续对话复用同一供应商,节省上下文成本
+              </p>
             </AlertDescription>
           </Alert>
 
@@ -252,11 +256,21 @@ export function SchedulingRulesDialog() {
             <Lightbulb className="h-4 w-4 text-primary" />
             <AlertTitle className="text-primary">最佳实践建议</AlertTitle>
             <AlertDescription className="space-y-1 text-sm text-foreground">
-              <p>• <strong>优先级设置</strong>:核心供应商设为 0,备用供应商设为 1-3</p>
-              <p>• <strong>权重配置</strong>:根据供应商容量设置权重(容量大 = 权重高)</p>
-              <p>• <strong>成本倍率</strong>:官方倍率为 1.0,自建服务可设置为 0.8-1.2</p>
-              <p>• <strong>限额设置</strong>:根据预算设置 5 小时、7 天、30 天限额</p>
-              <p>• <strong>并发控制</strong>:根据供应商 API 限制设置 Session 并发数</p>
+              <p>
+                • <strong>优先级设置</strong>:核心供应商设为 0,备用供应商设为 1-3
+              </p>
+              <p>
+                • <strong>权重配置</strong>:根据供应商容量设置权重(容量大 = 权重高)
+              </p>
+              <p>
+                • <strong>成本倍率</strong>:官方倍率为 1.0,自建服务可设置为 0.8-1.2
+              </p>
+              <p>
+                • <strong>限额设置</strong>:根据预算设置 5 小时、7 天、30 天限额
+              </p>
+              <p>
+                • <strong>并发控制</strong>:根据供应商 API 限制设置 Session 并发数
+              </p>
             </AlertDescription>
           </Alert>
         </div>

+ 2 - 5
src/app/settings/providers/page.tsx

@@ -17,10 +17,7 @@ export default async function SettingsProvidersPage() {
 
   return (
     <>
-      <SettingsPageHeader
-        title="供应商管理"
-        description="配置 API 服务商并维护可用状态。"
-      />
+      <SettingsPageHeader title="供应商管理" description="配置 API 服务商并维护可用状态。" />
 
       <Section
         title="服务商管理"
@@ -40,4 +37,4 @@ export default async function SettingsProvidersPage() {
       </Section>
     </>
   );
-}
+}

+ 10 - 14
src/app/usage-doc/layout.tsx

@@ -1,22 +1,18 @@
-import { Metadata } from 'next'
-import { getSession } from "@/lib/auth"
-import { DashboardHeader } from "../dashboard/_components/dashboard-header"
+import { Metadata } from "next";
+import { getSession } from "@/lib/auth";
+import { DashboardHeader } from "../dashboard/_components/dashboard-header";
 
 export const metadata: Metadata = {
-  title: '使用文档 - Claude Code Hub',
-  description: 'Claude Code Hub API 代理服务使用文档和指南',
-}
+  title: "使用文档 - Claude Code Hub",
+  description: "Claude Code Hub API 代理服务使用文档和指南",
+};
 
 /**
  * 文档页面布局
  * 提供文档页面的容器、样式和共用头部
  */
-export default async function UsageDocLayout({
-  children,
-}: {
-  children: React.ReactNode
-}) {
-  const session = await getSession()
+export default async function UsageDocLayout({ children }: { children: React.ReactNode }) {
+  const session = await getSession();
 
   return (
     <div className="min-h-screen bg-background">
@@ -29,5 +25,5 @@ export default async function UsageDocLayout({
         {children}
       </main>
     </div>
-  )
-}
+  );
+}

+ 233 - 106
src/app/usage-doc/page.tsx

@@ -1,27 +1,27 @@
-'use client'
+"use client";
 
-import { useState, useEffect } from 'react'
-import { cn } from '@/lib/utils'
-import { Skeleton } from '@/components/ui/skeleton'
+import { useState, useEffect } from "react";
+import { cn } from "@/lib/utils";
+import { Skeleton } from "@/components/ui/skeleton";
 
 /**
  * 文档目录项
  */
 interface TocItem {
-  id: string
-  text: string
-  level: number
+  id: string;
+  text: string;
+  level: number;
 }
 
 const headingClasses = {
-  h2: 'scroll-m-20 text-2xl font-semibold leading-snug text-foreground',
-  h3: 'scroll-m-20 mt-8 text-xl font-semibold leading-snug text-foreground',
-  h4: 'scroll-m-20 mt-6 text-lg font-semibold leading-snug text-foreground',
-} as const
+  h2: "scroll-m-20 text-2xl font-semibold leading-snug text-foreground",
+  h3: "scroll-m-20 mt-8 text-xl font-semibold leading-snug text-foreground",
+  h4: "scroll-m-20 mt-6 text-lg font-semibold leading-snug text-foreground",
+} as const;
 
 interface CodeBlockProps {
-  code: string
-  language: string
+  code: string;
+  language: string;
 }
 
 function CodeBlock({ code, language }: CodeBlockProps) {
@@ -32,19 +32,18 @@ function CodeBlock({ code, language }: CodeBlockProps) {
     >
       <code className="block whitespace-pre leading-relaxed">{code.trim()}</code>
     </pre>
-  )
+  );
 }
 
 interface UsageDocContentProps {
-  origin: string
+  origin: string;
 }
 
 function UsageDocContent({ origin }: UsageDocContentProps) {
-  const resolvedOrigin = origin || '当前站点地址'
+  const resolvedOrigin = origin || "当前站点地址";
 
   return (
     <article className="space-y-12 text-[15px] leading-6 text-muted-foreground">
-
       <section className="space-y-6">
         <h2 id="quick-start" className={headingClasses.h2}>
           🚀 快速开始
@@ -83,7 +82,13 @@ function UsageDocContent({ origin }: UsageDocContentProps) {
           </h3>
           <div className="space-y-3">
             <h4 className={headingClasses.h4}>1. 创建配置文件</h4>
-            <p>根据您的操作系统,在对应位置创建 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">settings.json</code> 文件:</p>
+            <p>
+              根据您的操作系统,在对应位置创建{" "}
+              <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                settings.json
+              </code>{" "}
+              文件:
+            </p>
             <div className="space-y-3">
               <div>
                 <p className="font-semibold text-foreground">macOS / Linux</p>
@@ -98,7 +103,13 @@ function UsageDocContent({ origin }: UsageDocContentProps) {
 
           <div className="space-y-3">
             <h4 className={headingClasses.h4}>2. 添加配置内容</h4>
-            <p>将以下配置复制到 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">settings.json</code> 文件中:</p>
+            <p>
+              将以下配置复制到{" "}
+              <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                settings.json
+              </code>{" "}
+              文件中:
+            </p>
             <CodeBlock
               language="json"
               code={`{
@@ -120,7 +131,13 @@ function UsageDocContent({ origin }: UsageDocContentProps) {
             <h4 className={headingClasses.h4}>3. 替换 API 密钥</h4>
             <blockquote className="space-y-2 rounded-lg border-l-2 border-primary/50 bg-muted/40 px-4 py-3">
               <p className="font-semibold text-foreground">重要</p>
-              <p>请将配置中的 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">your-api-key-here</code> 替换为您的实际 API 密钥。</p>
+              <p>
+                请将配置中的{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  your-api-key-here
+                </code>{" "}
+                替换为您的实际 API 密钥。
+              </p>
               <p>密钥获取方式:登录控制台 → API 密钥管理 → 创建 / 查看密钥。</p>
             </blockquote>
           </div>
@@ -145,18 +162,15 @@ function UsageDocContent({ origin }: UsageDocContentProps) {
           </h3>
 
           <p>
-            Droid 是 Factory AI 开发的交互式终端 AI 编程助手,支持通过 Claude Code Hub 代理服务使用。
-            本指南将帮助你在 5 分钟内完成 Droid 的安装和配置。
+            Droid 是 Factory AI 开发的交互式终端 AI 编程助手,支持通过 Claude Code Hub
+            代理服务使用。 本指南将帮助你在 5 分钟内完成 Droid 的安装和配置。
           </p>
 
           <div className="space-y-3">
             <h4 className={headingClasses.h4}>安装 Droid</h4>
 
             <p className="font-semibold text-foreground">macOS / Linux</p>
-            <CodeBlock
-              language="bash"
-              code={`curl -fsSL https://app.factory.ai/cli | sh`}
-            />
+            <CodeBlock language="bash" code={`curl -fsSL https://app.factory.ai/cli | sh`} />
 
             <p className="font-semibold text-foreground">Windows</p>
             <CodeBlock
@@ -166,11 +180,14 @@ function UsageDocContent({ origin }: UsageDocContentProps) {
 
             <blockquote className="space-y-1 rounded-lg border-l-2 border-primary/50 bg-muted/40 px-4 py-3">
               <p className="font-semibold text-foreground">提示</p>
-              <p>Linux 用户需确保已安装 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">xdg-utils</code>:</p>
-              <CodeBlock
-                language="bash"
-                code={`sudo apt-get install xdg-utils`}
-              />
+              <p>
+                Linux 用户需确保已安装{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  xdg-utils
+                </code>
+                :
+              </p>
+              <CodeBlock language="bash" code={`sudo apt-get install xdg-utils`} />
             </blockquote>
           </div>
 
@@ -189,21 +206,53 @@ droid`}
             <h4 className={headingClasses.h4}>基本使用</h4>
             <p>启动后,你可以直接与 Droid 对话:</p>
             <ul className="list-disc space-y-2 pl-6">
-              <li>分析代码:<code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">analyze this codebase and explain the overall architecture</code></li>
-              <li>修改代码:<code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">add comprehensive logging to the main application startup</code></li>
-              <li>安全审计:<code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">audit this codebase for security vulnerabilities</code></li>
-              <li>Git 操作:<code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">review my uncommitted changes and suggest improvements</code></li>
+              <li>
+                分析代码:
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  analyze this codebase and explain the overall architecture
+                </code>
+              </li>
+              <li>
+                修改代码:
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  add comprehensive logging to the main application startup
+                </code>
+              </li>
+              <li>
+                安全审计:
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  audit this codebase for security vulnerabilities
+                </code>
+              </li>
+              <li>
+                Git 操作:
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  review my uncommitted changes and suggest improvements
+                </code>
+              </li>
             </ul>
           </div>
 
           <div className="space-y-3">
             <h4 className={headingClasses.h4}>常用快捷键</h4>
             <ul className="list-disc space-y-2 pl-6">
-              <li><strong>Enter</strong>: 发送消息</li>
-              <li><strong>Shift+Enter</strong>: 多行输入</li>
-              <li><strong>Shift+Tab</strong>: 切换模式</li>
-              <li><strong>?</strong>: 查看所有快捷键</li>
-              <li><strong>Ctrl+C</strong> 或输入 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">exit</code>: 退出</li>
+              <li>
+                <strong>Enter</strong>: 发送消息
+              </li>
+              <li>
+                <strong>Shift+Enter</strong>: 多行输入
+              </li>
+              <li>
+                <strong>Shift+Tab</strong>: 切换模式
+              </li>
+              <li>
+                <strong>?</strong>: 查看所有快捷键
+              </li>
+              <li>
+                <strong>Ctrl+C</strong> 或输入{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">exit</code>:
+                退出
+              </li>
             </ul>
           </div>
         </div>
@@ -213,15 +262,17 @@ droid`}
             🔗 Droid 使用 Claude Code Hub 接入
           </h3>
 
-          <p>
-            配置 Droid 连接到 Claude Code Hub 代理服务,使用自己的 API 密钥。
-          </p>
+          <p>配置 Droid 连接到 Claude Code Hub 代理服务,使用自己的 API 密钥。</p>
 
           <div className="space-y-3">
             <h4 className={headingClasses.h4}>1. 注册并登录 Droid</h4>
             <ol className="list-decimal space-y-2 pl-6">
               <li>下载并安装 Droid(参考上一节)</li>
-              <li>运行 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">droid</code> 命令</li>
+              <li>
+                运行{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">droid</code>{" "}
+                命令
+              </li>
               <li>按提示注册并登录 Factory 账号</li>
             </ol>
           </div>
@@ -232,8 +283,18 @@ droid`}
 
             <p className="font-semibold text-foreground">配置文件路径</p>
             <ul className="list-disc space-y-2 pl-6">
-              <li>macOS / Linux: <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">~/.factory/config.json</code></li>
-              <li>Windows: <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">%USERPROFILE%\.factory\config.json</code></li>
+              <li>
+                macOS / Linux:{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  ~/.factory/config.json
+                </code>
+              </li>
+              <li>
+                Windows:{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  %USERPROFILE%\.factory\config.json
+                </code>
+              </li>
             </ul>
 
             <p className="font-semibold text-foreground mt-3">配置内容</p>
@@ -264,7 +325,13 @@ droid`}
             <h4 className={headingClasses.h4}>3. 替换 API 密钥</h4>
             <blockquote className="space-y-2 rounded-lg border-l-2 border-primary/50 bg-muted/40 px-4 py-3">
               <p className="font-semibold text-foreground">重要</p>
-              <p>将 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">your-api-key-here</code> 替换为你在 Claude Code Hub 控制台创建的 API 密钥。</p>
+              <p>
+                将{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  your-api-key-here
+                </code>{" "}
+                替换为你在 Claude Code Hub 控制台创建的 API 密钥。
+              </p>
               <p>密钥获取:登录控制台 → 设置 → API 密钥管理 → 创建密钥</p>
             </blockquote>
           </div>
@@ -273,8 +340,14 @@ droid`}
             <h4 className={headingClasses.h4}>4. 选择模型</h4>
             <ol className="list-decimal space-y-2 pl-6">
               <li>重启 Droid</li>
-              <li>输入 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/model</code> 命令</li>
-              <li>选择 <strong>GPT-5-Codex [CCH]</strong> 或 <strong>Sonnet 4.5 [CCH]</strong></li>
+              <li>
+                输入{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/model</code>{" "}
+                命令
+              </li>
+              <li>
+                选择 <strong>GPT-5-Codex [CCH]</strong> 或 <strong>Sonnet 4.5 [CCH]</strong>
+              </li>
               <li>开始使用!</li>
             </ol>
           </div>
@@ -285,16 +358,24 @@ droid`}
             💻 Codex CLI Windows 部署指南
           </h3>
 
-          <p>
-            Codex CLI 是 OpenAI 官方的命令行 AI 编程助手,支持通过 Claude Code Hub 代理使用。
-          </p>
+          <p>Codex CLI 是 OpenAI 官方的命令行 AI 编程助手,支持通过 Claude Code Hub 代理使用。</p>
 
           <div className="space-y-3">
             <h4 className={headingClasses.h4}>一、安装 Node.js 环境</h4>
 
             <p className="font-semibold text-foreground">方法一:官网下载(推荐)</p>
             <ol className="list-decimal space-y-2 pl-6">
-              <li>访问 <a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer" className="font-medium text-primary underline">https://nodejs.org/</a></li>
+              <li>
+                访问{" "}
+                <a
+                  href="https://nodejs.org/"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                  className="font-medium text-primary underline"
+                >
+                  https://nodejs.org/
+                </a>
+              </li>
               <li>下载 LTS 版本(需 v18 或更高)</li>
               <li>双击 .msi 文件,按向导安装</li>
               <li>验证安装:</li>
@@ -324,10 +405,7 @@ scoop install nodejs`}
               code={`npm i -g @openai/codex --registry=https://registry.npmmirror.com`}
             />
             <p>验证安装:</p>
-            <CodeBlock
-              language="powershell"
-              code={`codex --version`}
-            />
+            <CodeBlock language="powershell" code={`codex --version`} />
           </div>
 
           <div className="space-y-3">
@@ -335,8 +413,20 @@ scoop install nodejs`}
 
             <p className="font-semibold text-foreground">方法一:编辑配置文件(推荐)</p>
             <ol className="list-decimal space-y-2 pl-6">
-              <li>打开文件资源管理器,找到 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">C:\Users\你的用户名\.codex</code> 文件夹(不存在则创建)</li>
-              <li>创建 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">config.toml</code> 文件</li>
+              <li>
+                打开文件资源管理器,找到{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  C:\Users\你的用户名\.codex
+                </code>{" "}
+                文件夹(不存在则创建)
+              </li>
+              <li>
+                创建{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  config.toml
+                </code>{" "}
+                文件
+              </li>
               <li>使用 Notepad 打开,添加以下内容:</li>
             </ol>
             <CodeBlock
@@ -373,7 +463,13 @@ network_access = true`}
             />
 
             <ol className="list-decimal space-y-2 pl-6" start={4}>
-              <li>创建 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">auth.json</code> 文件,添加:</li>
+              <li>
+                创建{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  auth.json
+                </code>{" "}
+                文件,添加:
+              </li>
             </ol>
             <CodeBlock
               language="json"
@@ -392,7 +488,13 @@ network_access = true`}
             <blockquote className="space-y-2 rounded-lg border-l-2 border-primary/50 bg-muted/40 px-4 py-3">
               <p className="font-semibold text-foreground">重要提示</p>
               <ul className="list-disc space-y-2 pl-4">
-                <li>将 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">your-api-key-here</code> 替换为你的 Claude Code Hub API 密钥</li>
+                <li>
+                  将{" "}
+                  <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                    your-api-key-here
+                  </code>{" "}
+                  替换为你的 Claude Code Hub API 密钥
+                </li>
                 <li>使用与 Claude Code 相同的密钥体系</li>
                 <li>设置环境变量后需重新打开 PowerShell 窗口</li>
               </ul>
@@ -414,7 +516,13 @@ codex`}
 
             <p className="font-semibold text-foreground">1. 命令未找到</p>
             <ul className="list-disc space-y-2 pl-6">
-              <li>确保 npm 全局路径(通常是 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">C:\Users\你的用户名\AppData\Roaming\npm</code>)已添加到系统 PATH</li>
+              <li>
+                确保 npm 全局路径(通常是{" "}
+                <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">
+                  C:\Users\你的用户名\AppData\Roaming\npm
+                </code>
+                )已添加到系统 PATH
+              </li>
               <li>重新打开 PowerShell 窗口</li>
             </ul>
 
@@ -425,7 +533,7 @@ codex`}
 echo $env:CCH_API_KEY
 
 # 测试网络连接
-Test-NetConnection -ComputerName ${resolvedOrigin.replace('https://', '').replace('http://', '')} -Port 443`}
+Test-NetConnection -ComputerName ${resolvedOrigin.replace("https://", "").replace("http://", "")} -Port 443`}
             />
 
             <p className="font-semibold text-foreground">3. 更新 Codex</p>
@@ -445,10 +553,22 @@ Test-NetConnection -ComputerName ${resolvedOrigin.replace('https://', '').replac
         </h2>
         <p>启动 Claude Code 后,您可以使用以下常用命令:</p>
         <ul className="list-disc space-y-2 pl-6">
-          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/help</code> - 查看帮助信息</li>
-          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/clear</code> - 清空对话历史,并开启新的对话</li>
-          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/compact</code> - 总结当前对话</li>
-          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/cost</code> - 查看当前对话已经使用的金额</li>
+          <li>
+            <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/help</code> -
+            查看帮助信息
+          </li>
+          <li>
+            <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/clear</code> -
+            清空对话历史,并开启新的对话
+          </li>
+          <li>
+            <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/compact</code> -
+            总结当前对话
+          </li>
+          <li>
+            <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/cost</code> -
+            查看当前对话已经使用的金额
+          </li>
           <li>
             ... 其他更多命令查看
             <a
@@ -488,7 +608,7 @@ Test-NetConnection -ComputerName ${resolvedOrigin.replace('https://', '').replac
         </div>
       </section>
     </article>
-  )
+  );
 }
 
 /**
@@ -496,81 +616,78 @@ Test-NetConnection -ComputerName ${resolvedOrigin.replace('https://', '').replac
  * 使用客户端组件渲染静态文档内容,并提供目录导航
  */
 export default function UsageDocPage() {
-  const [activeId, setActiveId] = useState<string>('')
-  const [tocItems, setTocItems] = useState<TocItem[]>([])
-  const [tocReady, setTocReady] = useState(false)
-  const [serviceOrigin, setServiceOrigin] = useState(() =>
-    (typeof window !== 'undefined' && window.location.origin) || ''
-  )
+  const [activeId, setActiveId] = useState<string>("");
+  const [tocItems, setTocItems] = useState<TocItem[]>([]);
+  const [tocReady, setTocReady] = useState(false);
+  const [serviceOrigin, setServiceOrigin] = useState(
+    () => (typeof window !== "undefined" && window.location.origin) || ""
+  );
 
   useEffect(() => {
-    setServiceOrigin(window.location.origin)
-  }, [])
+    setServiceOrigin(window.location.origin);
+  }, []);
 
   // 生成目录并监听滚动
   useEffect(() => {
     // 获取所有标题
-    const headings = document.querySelectorAll('h2, h3')
-    const items: TocItem[] = []
+    const headings = document.querySelectorAll("h2, h3");
+    const items: TocItem[] = [];
 
     headings.forEach((heading) => {
       // 为标题添加 id(如果没有的话)
       if (!heading.id) {
-        heading.id = heading.textContent?.toLowerCase().replace(/\s+/g, '-') || ''
+        heading.id = heading.textContent?.toLowerCase().replace(/\s+/g, "-") || "";
       }
 
       items.push({
         id: heading.id,
-        text: heading.textContent || '',
-        level: parseInt(heading.tagName[1])
-      })
-    })
+        text: heading.textContent || "",
+        level: parseInt(heading.tagName[1]),
+      });
+    });
 
-    setTocItems(items)
-    setTocReady(true)
+    setTocItems(items);
+    setTocReady(true);
 
     // 监听滚动,高亮当前章节
     const handleScroll = () => {
-      const scrollPosition = window.scrollY + 100
+      const scrollPosition = window.scrollY + 100;
 
       for (const item of items) {
-        const element = document.getElementById(item.id)
+        const element = document.getElementById(item.id);
         if (element && element.offsetTop <= scrollPosition) {
-          setActiveId(item.id)
+          setActiveId(item.id);
         }
       }
-    }
+    };
 
-    window.addEventListener('scroll', handleScroll)
-    handleScroll() // 初始化
+    window.addEventListener("scroll", handleScroll);
+    handleScroll(); // 初始化
 
-    return () => window.removeEventListener('scroll', handleScroll)
-  }, [])
+    return () => window.removeEventListener("scroll", handleScroll);
+  }, []);
 
   // 点击目录项滚动到对应位置
   const scrollToSection = (id: string) => {
-    const element = document.getElementById(id)
+    const element = document.getElementById(id);
     if (element) {
-      const offsetTop = element.offsetTop - 80
+      const offsetTop = element.offsetTop - 80;
       window.scrollTo({
         top: offsetTop,
-        behavior: 'smooth'
-      })
+        behavior: "smooth",
+      });
     }
-  }
+  };
 
   return (
     <div className="relative flex gap-8">
       {/* 左侧主文档 */}
       <div className="flex-1">
-        
-
         {/* 文档容器 */}
         <div className="relative bg-card rounded-xl shadow-sm border p-8 md:p-12">
           {/* 文档内容 */}
           <UsageDocContent origin={serviceOrigin} />
         </div>
-
       </div>
 
       {/* 右侧目录导航 */}
@@ -589,7 +706,8 @@ export default function UsageDocPage() {
               {tocReady && tocItems.length === 0 && (
                 <p className="text-xs text-muted-foreground">本页暂无可用章节</p>
               )}
-              {tocReady && tocItems.length > 0 &&
+              {tocReady &&
+                tocItems.length > 0 &&
                 tocItems.map((item) => (
                   <button
                     key={item.id}
@@ -612,11 +730,20 @@ export default function UsageDocPage() {
           <div className="bg-card rounded-lg border p-4">
             <h4 className="font-semibold text-sm mb-3">快速链接</h4>
             <div className="space-y-2">
-              <a href="/dashboard" className="block text-sm text-muted-foreground hover:text-primary transition-colors">
+              <a
+                href="/dashboard"
+                className="block text-sm text-muted-foreground hover:text-primary transition-colors"
+              >
                 返回仪表盘
               </a>
-              <a href="#" onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }) }}
-                 className="block text-sm text-muted-foreground hover:text-primary transition-colors">
+              <a
+                href="#"
+                onClick={(e) => {
+                  e.preventDefault();
+                  window.scrollTo({ top: 0, behavior: "smooth" });
+                }}
+                className="block text-sm text-muted-foreground hover:text-primary transition-colors"
+              >
                 回到顶部
               </a>
             </div>
@@ -624,5 +751,5 @@ export default function UsageDocPage() {
         </div>
       </aside>
     </div>
-  )
+  );
 }

+ 3 - 3
src/app/v1/[...route]/route.ts

@@ -1,4 +1,5 @@
 import { Hono } from "hono";
+import { logger } from '@/lib/logger';
 import { handle } from "hono/vercel";
 import { handleProxyRequest } from "@/app/v1/_lib/proxy-handler";
 import { handleChatCompletions } from "@/app/v1/_lib/codex/chat-completions-handler";
@@ -8,7 +9,7 @@ export const runtime = "nodejs";
 
 // 初始化 SessionTracker(清理旧 Set 格式数据)
 SessionTracker.initialize().catch((err) => {
-  console.error('[App] SessionTracker initialization failed:', err);
+  logger.error('[App] SessionTracker initialization failed:', err);
 });
 
 const app = new Hono().basePath("/v1");
@@ -17,8 +18,7 @@ const app = new Hono().basePath("/v1");
 app.post("/chat/completions", handleChatCompletions);
 
 // Response API 路由(支持 Codex)
-app.post("/responses", handleChatCompletions);  // OpenAI 
-
+app.post("/responses", handleChatCompletions); // OpenAI
 
 // Claude API 和其他所有请求(fallback)
 app.all("*", handleProxyRequest);

+ 62 - 48
src/app/v1/_lib/codex/chat-completions-handler.ts

@@ -1,4 +1,5 @@
 import type { Context } from "hono";
+import { logger } from '@/lib/logger';
 import { ProxySession } from "../proxy/session";
 import { ProxyAuthenticator } from "../proxy/auth-guard";
 import { ProxyRateLimitGuard } from "../proxy/rate-limit-guard";
@@ -23,7 +24,7 @@ import type { ChatCompletionRequest } from "./types/compatible";
  * 5. 响应自动转换回 OpenAI 格式(在 ResponseHandler 中)
  */
 export async function handleChatCompletions(c: Context): Promise<Response> {
-  console.info('[ChatCompletions] Received OpenAI Compatible API request');
+  logger.info('[ChatCompletions] Received OpenAI Compatible API request');
 
   const session = await ProxySession.fromContext(c);
 
@@ -31,19 +32,20 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
     const request = session.request.message;
 
     // 格式检测
-    const isOpenAIFormat = 'messages' in request && Array.isArray(request.messages);
-    const isResponseAPIFormat = 'input' in request && Array.isArray(request.input);
+    const isOpenAIFormat = "messages" in request && Array.isArray(request.messages);
+    const isResponseAPIFormat = "input" in request && Array.isArray(request.input);
 
     if (!isOpenAIFormat && !isResponseAPIFormat) {
       return new Response(
         JSON.stringify({
           error: {
-            message: 'Invalid request: either "messages" (OpenAI format) or "input" (Response API format) is required',
-            type: 'invalid_request_error',
-            code: 'missing_required_fields'
-          }
+            message:
+              'Invalid request: either "messages" (OpenAI format) or "input" (Response API format) is required',
+            type: "invalid_request_error",
+            code: "missing_required_fields",
+          },
         }),
-        { status: 400, headers: { 'Content-Type': 'application/json' } }
+        { status: 400, headers: { "Content-Type": "application/json" } }
       );
     }
 
@@ -55,16 +57,16 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
         return new Response(
           JSON.stringify({
             error: {
-              message: 'Invalid request: model is required',
-              type: 'invalid_request_error',
-              code: 'missing_required_fields'
-            }
+              message: "Invalid request: model is required",
+              type: "invalid_request_error",
+              code: "missing_required_fields",
+            },
           }),
-          { status: 400, headers: { 'Content-Type': 'application/json' } }
+          { status: 400, headers: { "Content-Type": "application/json" } }
         );
       }
 
-      console.debug('[ChatCompletions] OpenAI format detected, transforming...', {
+      logger.debug('[ChatCompletions] OpenAI format detected, transforming...', {
         model: openAIRequest.model,
         stream: openAIRequest.stream,
         messageCount: openAIRequest.messages.length,
@@ -76,14 +78,17 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
       });
 
       // 开发模式:输出完整原始请求
-      if (process.env.NODE_ENV === 'development') {
-        console.debug('[ChatCompletions] Full OpenAI request:', JSON.stringify(openAIRequest, null, 2));
+      if (process.env.NODE_ENV === "development") {
+        console.debug(
+          "[ChatCompletions] Full OpenAI request:",
+          JSON.stringify(openAIRequest, null, 2)
+        );
       }
 
       try {
         let responseRequest = RequestTransformer.transform(openAIRequest);
 
-        console.debug('[ChatCompletions] Transformed to Response API:', {
+        logger.debug('[ChatCompletions] Transformed to Response API:', {
           model: responseRequest.model,
           inputCount: responseRequest.input?.length,
           hasReasoning: !!responseRequest.reasoning,
@@ -95,17 +100,26 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
         });
 
         // 开发模式:输出完整转换后请求
-        if (process.env.NODE_ENV === 'development') {
-          console.debug('[ChatCompletions] Full Response API request:', JSON.stringify(responseRequest, null, 2));
+        if (process.env.NODE_ENV === "development") {
+          console.debug(
+            "[ChatCompletions] Full Response API request:",
+            JSON.stringify(responseRequest, null, 2)
+          );
         }
 
         // 适配 Codex CLI (注入 instructions)
-        if (process.env.NODE_ENV === 'development') {
-          console.debug('[ChatCompletions] Before adaptForCodexCLI:', JSON.stringify(responseRequest, null, 2));
+        if (process.env.NODE_ENV === "development") {
+          console.debug(
+            "[ChatCompletions] Before adaptForCodexCLI:",
+            JSON.stringify(responseRequest, null, 2)
+          );
         }
         responseRequest = adaptForCodexCLI(responseRequest);
-        if (process.env.NODE_ENV === 'development') {
-          console.debug('[ChatCompletions] After adaptForCodexCLI:', JSON.stringify(responseRequest, null, 2));
+        if (process.env.NODE_ENV === "development") {
+          console.debug(
+            "[ChatCompletions] After adaptForCodexCLI:",
+            JSON.stringify(responseRequest, null, 2)
+          );
         }
 
         // 更新 session(替换为 Response API 格式)
@@ -113,50 +127,51 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
         session.request.model = responseRequest.model;
 
         // 验证转换结果(仅在开发环境)
-        if (process.env.NODE_ENV === 'development') {
+        if (process.env.NODE_ENV === "development") {
           const msgObj = session.request.message as Record<string, unknown>;
-          console.debug('[ChatCompletions] Verification - session.request.message contains input:', {
-            hasInput: 'input' in msgObj,
-            inputType: Array.isArray(msgObj.input) ? 'array' : typeof msgObj.input,
-            inputLength: Array.isArray(msgObj.input) ? msgObj.input.length : 'N/A'
-          });
+          console.debug(
+            "[ChatCompletions] Verification - session.request.message contains input:",
+            {
+              hasInput: "input" in msgObj,
+              inputType: Array.isArray(msgObj.input) ? "array" : typeof msgObj.input,
+              inputLength: Array.isArray(msgObj.input) ? msgObj.input.length : "N/A",
+            }
+          );
         }
 
         // 标记为 OpenAI 格式(用于响应转换)
-        session.setOriginalFormat('openai');
-
+        session.setOriginalFormat("openai");
       } catch (transformError) {
-        console.error('[ChatCompletions] Request transformation failed:', transformError);
+        logger.error('[ChatCompletions] Request transformation failed:', { context: transformError });
         return new Response(
           JSON.stringify({
             error: {
-              message: 'Failed to transform request format',
-              type: 'invalid_request_error',
-              code: 'transformation_error'
-            }
+              message: "Failed to transform request format",
+              type: "invalid_request_error",
+              code: "transformation_error",
+            },
           }),
-          { status: 400, headers: { 'Content-Type': 'application/json' } }
+          { status: 400, headers: { "Content-Type": "application/json" } }
         );
       }
-
     } else if (isResponseAPIFormat) {
       // Response API 格式 → 直接透传
-      console.info('[ChatCompletions] Response API format detected, passing through');
+      logger.info('[ChatCompletions] Response API format detected, passing through');
 
       // 标记为 Response API 格式(响应也用 Response API 格式)
-      session.setOriginalFormat('response');
+      session.setOriginalFormat("response");
 
       // 验证必需字段
       if (!request.model) {
         return new Response(
           JSON.stringify({
             error: {
-              message: 'Invalid request: model is required',
-              type: 'invalid_request_error',
-              code: 'missing_required_fields'
-            }
+              message: "Invalid request: model is required",
+              type: "invalid_request_error",
+              code: "missing_required_fields",
+            },
           }),
-          { status: 400, headers: { 'Content-Type': 'application/json' } }
+          { status: 400, headers: { "Content-Type": "application/json" } }
         );
       }
     }
@@ -175,7 +190,7 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
     }
 
     // 3. 供应商选择(指定 Codex 类型)
-    const providerUnavailable = await ProxyProviderResolver.ensure(session, 'codex');
+    const providerUnavailable = await ProxyProviderResolver.ensure(session, "codex");
     if (providerUnavailable) {
       return providerUnavailable;
     }
@@ -201,9 +216,8 @@ export async function handleChatCompletions(c: Context): Promise<Response> {
 
     // 5. 响应处理(自动转换回 OpenAI 格式)
     return await ProxyResponseHandler.dispatch(session, response);
-
   } catch (error) {
-    console.error("[ChatCompletions] Handler error:", error);
+    logger.error('[ChatCompletions] Handler error:', error);
     return await ProxyErrorHandler.handle(session, error);
   }
 }

+ 12 - 11
src/app/v1/_lib/codex/codex-cli-adapter.ts

@@ -13,8 +13,9 @@
  * - 默认开启,如测试发现问题可通过 ENABLE_CODEX_CLI_INJECTION 关闭
  */
 
-import type { ResponseRequest } from './types/response';
-import { CODEX_CLI_INSTRUCTIONS, isCodexCLIRequest } from './constants/codex-cli-instructions';
+import type { ResponseRequest } from "./types/response";
+import { logger } from '@/lib/logger';
+import { CODEX_CLI_INSTRUCTIONS, isCodexCLIRequest } from "./constants/codex-cli-instructions";
 
 /**
  * 功能开关
@@ -33,10 +34,10 @@ export const ENABLE_CODEX_CLI_INJECTION = false;
  * Codex CLI 不支持以下字段,需要在注入 instructions 时删除
  */
 const INCOMPATIBLE_FIELDS: Array<keyof ResponseRequest> = [
-  'temperature',
-  'top_p',
-  'user',
-  'truncation',
+  "temperature",
+  "top_p",
+  "user",
+  "truncation",
   // 注意: max_output_tokens 根据测试决定是否删除
   // 注意: reasoning 保留(Codex 核心功能)
   // 注意: tools 保留(Codex 支持 function calls)
@@ -60,12 +61,12 @@ export function adaptForCodexCLI(request: ResponseRequest): ResponseRequest {
 
   // 步骤 1: 注入 instructions (如果开关启用)
   if (ENABLE_CODEX_CLI_INJECTION && !isCodexCLIRequest(request.instructions)) {
-    console.info('[CodexCLI] Non-Codex CLI request detected, injecting instructions');
+    logger.info('[CodexCLI] Non-Codex CLI request detected, injecting instructions');
     adaptedRequest.instructions = CODEX_CLI_INSTRUCTIONS;
   } else if (ENABLE_CODEX_CLI_INJECTION) {
-    console.info('[CodexCLI] Codex CLI request detected, skipping injection');
+    logger.info('[CodexCLI] Codex CLI request detected, skipping injection');
   } else {
-    console.info('[CodexCLI] Injection disabled, skipping instructions');
+    logger.info('[CodexCLI] Injection disabled, skipping instructions');
   }
 
   // 步骤 2: 删除不兼容字段 (总是执行)
@@ -78,10 +79,10 @@ export function adaptForCodexCLI(request: ResponseRequest): ResponseRequest {
   }
 
   if (removedFields.length > 0) {
-    console.debug(`[CodexCLI] Removed incompatible fields: ${removedFields.join(', ')}`);
+    console.debug(`[CodexCLI] Removed incompatible fields: ${removedFields.join(", ")}`);
   }
 
-  console.debug('[CodexCLI] Adapted request:', {
+  logger.debug('[CodexCLI] Adapted request:', {
     hasInstructions: !!adaptedRequest.instructions,
     instructionsLength: adaptedRequest.instructions?.length,
     removedFields,

+ 3 - 3
src/app/v1/_lib/codex/constants/codex-cli-instructions.ts

@@ -1,9 +1,9 @@
 /**
  * Codex CLI System Instructions
- * 
+ *
  * 来源: claude-relay-service/src/routes/openaiRoutes.js
  * 用途: 为非 Codex CLI 客户端注入标准的 Codex CLI system prompt
- * 
+ *
  * ⚠️ 注意: 这是一个存疑功能,可能并非所有 Codex 请求都需要这个 prompt
  * 但根据 claude-relay-service 的实践,注入此 prompt 可以提高兼容性
  */
@@ -350,5 +350,5 @@ If all steps are complete, ensure you call \`update_plan\` to mark all steps as
  */
 export function isCodexCLIRequest(instructions?: string): boolean {
   if (!instructions) return false;
-  return instructions.startsWith('You are a coding agent running in the Codex CLI');
+  return instructions.startsWith("You are a coding agent running in the Codex CLI");
 }

+ 16 - 16
src/app/v1/_lib/codex/transformers/request.ts

@@ -13,7 +13,7 @@ import type {
   ContentPart,
   ChatCompletionTool,
   ToolChoiceObject,
-} from '../types/compatible';
+} from "../types/compatible";
 import type {
   ResponseRequest,
   InputItem,
@@ -21,7 +21,7 @@ import type {
   MessageInput,
   ResponseTool,
   ToolChoiceObject as ResponseToolChoiceObject,
-} from '../types/response';
+} from "../types/response";
 
 export class RequestTransformer {
   /**
@@ -37,10 +37,10 @@ export class RequestTransformer {
       model: request.model,
       input: this.transformMessages(request.messages),
       reasoning: {
-        effort: 'medium' as const,
-        summary: 'auto' as const
+        effort: "medium" as const,
+        summary: "auto" as const,
       },
-      stream: request.stream
+      stream: request.stream,
     };
   }
 
@@ -56,7 +56,7 @@ export class RequestTransformer {
    * 关键: system role → developer role
    */
   private static transformMessage(message: ChatMessage): MessageInput {
-    const role = message.role === 'system' ? 'developer' : message.role;
+    const role = message.role === "system" ? "developer" : message.role;
     return {
       role: role,
       content: this.transformContent(message.content, role),
@@ -71,13 +71,13 @@ export class RequestTransformer {
    */
   private static transformContent(
     content: string | ContentPart[],
-    role: 'user' | 'assistant' | 'developer'
+    role: "user" | "assistant" | "developer"
   ): ContentItem[] {
     // 字符串 → [{ type: 'input_text' | 'output_text', text }]
-    if (typeof content === 'string') {
+    if (typeof content === "string") {
       return [
         {
-          type: role === 'assistant' ? 'output_text' : 'input_text',
+          type: role === "assistant" ? "output_text" : "input_text",
           text: content,
         },
       ];
@@ -95,17 +95,17 @@ export class RequestTransformer {
    */
   private static transformContentPart(
     part: ContentPart,
-    role: 'user' | 'assistant' | 'developer'
+    role: "user" | "assistant" | "developer"
   ): ContentItem {
-    if (part.type === 'text') {
+    if (part.type === "text") {
       return {
-        type: role === 'assistant' ? 'output_text' : 'input_text',
+        type: role === "assistant" ? "output_text" : "input_text",
         text: part.text!,
       };
     } else {
       // image_url → input_image(图片总是 input)
       return {
-        type: 'input_image',
+        type: "input_image",
         image_url: part.image_url!.url,
       };
     }
@@ -117,7 +117,7 @@ export class RequestTransformer {
    */
   private static transformTools(tools: ChatCompletionTool[]): ResponseTool[] {
     return tools.map((tool) => ({
-      type: 'function',
+      type: "function",
       function: {
         name: tool.function.name,
         description: tool.function.description,
@@ -134,11 +134,11 @@ export class RequestTransformer {
   private static transformToolChoice(
     toolChoice: string | ToolChoiceObject
   ): string | ResponseToolChoiceObject {
-    if (typeof toolChoice === 'string') {
+    if (typeof toolChoice === "string") {
       return toolChoice;
     }
     return {
-      type: 'function',
+      type: "function",
       function: toolChoice.function,
     };
   }

+ 19 - 31
src/app/v1/_lib/codex/transformers/response.ts

@@ -3,16 +3,8 @@
  * 纯函数实现,无副作用
  */
 
-import type {
-  ChatCompletionResponse,
-  ChatCompletionChoice,
-} from '../types/compatible';
-import type {
-  ResponseObject,
-  OutputItem,
-  ReasoningOutput,
-  MessageOutput,
-} from '../types/response';
+import type { ChatCompletionResponse, ChatCompletionChoice } from "../types/compatible";
+import type { ResponseObject, OutputItem, ReasoningOutput, MessageOutput } from "../types/response";
 
 export class ResponseTransformer {
   /**
@@ -23,20 +15,18 @@ export class ResponseTransformer {
     const mainText = this.extractMainText(response.output);
 
     // 拼接思考内容: <think>...</think>\n主要内容
-    const fullContent = thinkingText
-      ? `<think>${thinkingText}</think>\n${mainText}`
-      : mainText;
+    const fullContent = thinkingText ? `<think>${thinkingText}</think>\n${mainText}` : mainText;
 
     return {
       id: this.convertId(response.id),
-      object: 'chat.completion',
+      object: "chat.completion",
       created: response.created,
       model: response.model,
       choices: [
         {
           index: 0,
           message: {
-            role: 'assistant',
+            role: "assistant",
             content: fullContent,
           },
           finish_reason: this.mapFinishReason(response.status),
@@ -55,9 +45,9 @@ export class ResponseTransformer {
    * 提取思考内容(reasoning summary)
    */
   private static extractThinking(output: OutputItem[]): string | null {
-    const reasoningItem = output.find(
-      (item) => item.type === 'reasoning'
-    ) as ReasoningOutput | undefined;
+    const reasoningItem = output.find((item) => item.type === "reasoning") as
+      | ReasoningOutput
+      | undefined;
 
     if (!reasoningItem || !reasoningItem.summary) {
       return null;
@@ -71,39 +61,37 @@ export class ResponseTransformer {
    * 提取主要文本内容(message output)
    */
   private static extractMainText(output: OutputItem[]): string {
-    const messageItem = output.find(
-      (item) => item.type === 'message'
-    ) as MessageOutput | undefined;
+    const messageItem = output.find((item) => item.type === "message") as MessageOutput | undefined;
 
     if (!messageItem) {
-      return '';
+      return "";
     }
 
     // 拼接所有 output_text
     return messageItem.content
-      .filter((c) => c.type === 'output_text')
+      .filter((c) => c.type === "output_text")
       .map((c) => c.text)
-      .join('');
+      .join("");
   }
 
   /**
    * 转换响应 ID: resp_xxx → chatcmpl-xxx
    */
   private static convertId(responseId: string): string {
-    return responseId.replace('resp_', 'chatcmpl-');
+    return responseId.replace("resp_", "chatcmpl-");
   }
 
   /**
    * 映射 finish_reason
    */
   private static mapFinishReason(
-    status: ResponseObject['status']
-  ): ChatCompletionChoice['finish_reason'] {
+    status: ResponseObject["status"]
+  ): ChatCompletionChoice["finish_reason"] {
     switch (status) {
-      case 'completed':
-        return 'stop';
-      case 'incomplete':
-        return 'length';
+      case "completed":
+        return "stop";
+      case "incomplete":
+        return "length";
       default:
         return null;
     }

+ 53 - 62
src/app/v1/_lib/codex/transformers/stream.ts

@@ -3,7 +3,7 @@
  * 有状态的转换器,需要维护元数据和 <think> 标签状态
  */
 
-import type { ChatCompletionChunk } from '../types/compatible';
+import type { ChatCompletionChunk } from "../types/compatible";
 import type {
   SSEEvent,
   ResponseCreatedEvent,
@@ -11,12 +11,12 @@ import type {
   ReasoningSummaryTextDeltaEvent,
   ReasoningSummaryTextDoneEvent,
   ReasoningSummaryPartDoneEvent,
-} from '../types/response';
+} from "../types/response";
 
 export class StreamTransformer {
-  private chunkId: string = '';
+  private chunkId: string = "";
   private created: number = Math.floor(Date.now() / 1000); // Unix 秒时间戳
-  private model: string = 'gpt-5';
+  private model: string = "gpt-5";
 
   // 状态追踪:用于 reasoning 标签包装
   private isInReasoning: boolean = false;
@@ -27,11 +27,14 @@ export class StreamTransformer {
    * 返回 null 表示跳过该事件
    * 返回数组表示需要发送多个 chunk(如在 completed 时关闭 think 标签)
    */
-  transform(
-    event: SSEEvent
-  ): ChatCompletionChunk | ChatCompletionChunk[] | null {
+  transform(event: SSEEvent): ChatCompletionChunk | ChatCompletionChunk[] | null {
     // 懒初始化:从任何事件中提取 ID
-    if (!this.chunkId && typeof event.data === 'object' && event.data !== null && 'item_id' in event.data) {
+    if (
+      !this.chunkId &&
+      typeof event.data === "object" &&
+      event.data !== null &&
+      "item_id" in event.data
+    ) {
       const itemId = (event.data as { item_id?: string }).item_id;
       if (itemId) {
         this.chunkId = this.convertItemId(itemId);
@@ -39,31 +42,25 @@ export class StreamTransformer {
     }
 
     switch (event.event) {
-      case 'response.created':
+      case "response.created":
         return this.handleCreated(event.data as ResponseCreatedEvent);
 
-      case 'response.output_text.delta':
+      case "response.output_text.delta":
         return this.handleTextDelta(event.data as OutputTextDeltaEvent);
 
-      case 'response.reasoning_summary_text.delta':
-        return this.handleReasoningDelta(
-          event.data as ReasoningSummaryTextDeltaEvent
-        );
+      case "response.reasoning_summary_text.delta":
+        return this.handleReasoningDelta(event.data as ReasoningSummaryTextDeltaEvent);
 
-      case 'response.reasoning_summary_text.done':
-        return this.handleReasoningDone(
-          event.data as ReasoningSummaryTextDoneEvent
-        );
+      case "response.reasoning_summary_text.done":
+        return this.handleReasoningDone(event.data as ReasoningSummaryTextDoneEvent);
 
-      case 'response.reasoning_summary_part.done':
-        return this.handleReasoningPartDone(
-          event.data as ReasoningSummaryPartDoneEvent
-        );
+      case "response.reasoning_summary_part.done":
+        return this.handleReasoningPartDone(event.data as ReasoningSummaryPartDoneEvent);
 
-      case 'response.completed':
+      case "response.completed":
         return this.handleCompleted();
 
-      case 'response.failed':
+      case "response.failed":
         this.resetAfterCompletion();
         return null;
 
@@ -85,13 +82,13 @@ export class StreamTransformer {
 
     return {
       id: this.chunkId,
-      object: 'chat.completion.chunk',
+      object: "chat.completion.chunk",
       created: this.created,
       model: this.model,
       choices: [
         {
           index: 0,
-          delta: { role: 'assistant' },
+          delta: { role: "assistant" },
           finish_reason: null,
         },
       ],
@@ -104,43 +101,37 @@ export class StreamTransformer {
    * 根据 item_id 前缀判断是否需要添加 <think> 标签
    */
   private handleTextDelta(data: OutputTextDeltaEvent): ChatCompletionChunk {
-    const itemId = data.item_id || '';
-    return this.emitContentDelta(itemId, data.delta, 'message');
+    const itemId = data.item_id || "";
+    return this.emitContentDelta(itemId, data.delta, "message");
   }
 
   /**
    * 处理 reasoning summary 的增量事件
    */
-  private handleReasoningDelta(
-    data: ReasoningSummaryTextDeltaEvent
-  ): ChatCompletionChunk {
-    const itemId = data.item_id || '';
-    return this.emitContentDelta(itemId, data.delta, 'reasoning');
+  private handleReasoningDelta(data: ReasoningSummaryTextDeltaEvent): ChatCompletionChunk {
+    const itemId = data.item_id || "";
+    return this.emitContentDelta(itemId, data.delta, "reasoning");
   }
 
   /**
    * 处理 reasoning summary 的完成事件(可能只有最终文本)
    */
-  private handleReasoningDone(
-    data: ReasoningSummaryTextDoneEvent
-  ): ChatCompletionChunk | null {
-    const itemId = data.item_id || '';
+  private handleReasoningDone(data: ReasoningSummaryTextDoneEvent): ChatCompletionChunk | null {
+    const itemId = data.item_id || "";
     if (!data.text) {
       return null;
     }
     if (itemId && this.reasoningItemsWithOutput.has(itemId)) {
       return null;
     }
-    return this.emitContentDelta(itemId, data.text, 'reasoning');
+    return this.emitContentDelta(itemId, data.text, "reasoning");
   }
 
   /**
    * 处理 reasoning summary part 完成事件(部分文本)
    */
-  private handleReasoningPartDone(
-    data: ReasoningSummaryPartDoneEvent
-  ): ChatCompletionChunk | null {
-    const itemId = data.item_id || '';
+  private handleReasoningPartDone(data: ReasoningSummaryPartDoneEvent): ChatCompletionChunk | null {
+    const itemId = data.item_id || "";
     const text = data.part?.text;
     if (!text) {
       return null;
@@ -148,7 +139,7 @@ export class StreamTransformer {
     if (itemId && this.reasoningItemsWithOutput.has(itemId)) {
       return null;
     }
-    return this.emitContentDelta(itemId, text, 'reasoning');
+    return this.emitContentDelta(itemId, text, "reasoning");
   }
 
   /**
@@ -163,13 +154,13 @@ export class StreamTransformer {
     if (this.isInReasoning) {
       chunks.push({
         id: this.chunkId,
-        object: 'chat.completion.chunk',
+        object: "chat.completion.chunk",
         created: this.created,
         model: this.model,
         choices: [
           {
             index: 0,
-            delta: { content: '</think>' },
+            delta: { content: "</think>" },
             finish_reason: null,
           },
         ],
@@ -180,14 +171,14 @@ export class StreamTransformer {
     // 添加结束 chunk
     const endChunk: ChatCompletionChunk = {
       id: this.chunkId,
-      object: 'chat.completion.chunk',
+      object: "chat.completion.chunk",
       created: this.created,
       model: this.model,
       choices: [
         {
           index: 0,
           delta: {},
-          finish_reason: 'stop',
+          finish_reason: "stop",
         },
       ],
     };
@@ -201,32 +192,32 @@ export class StreamTransformer {
   private emitContentDelta(
     rawItemId: string,
     delta: string,
-    source: 'reasoning' | 'message'
+    source: "reasoning" | "message"
   ): ChatCompletionChunk {
     this.ensureChunkMetadata(rawItemId);
 
-    const itemId = rawItemId || '';
-    const isReasoning = source === 'reasoning' || itemId.startsWith('rs_');
-    const isMessage = source === 'message' || itemId.startsWith('msg_');
+    const itemId = rawItemId || "";
+    const isReasoning = source === "reasoning" || itemId.startsWith("rs_");
+    const isMessage = source === "message" || itemId.startsWith("msg_");
 
     let content = delta;
 
     if (isReasoning) {
       if (!this.isInReasoning) {
-        content = '<think>' + content;
+        content = "<think>" + content;
         this.isInReasoning = true;
       }
       if (itemId) {
         this.reasoningItemsWithOutput.add(itemId);
       }
     } else if (isMessage && this.isInReasoning) {
-      content = '</think>\n' + content;
+      content = "</think>\n" + content;
       this.isInReasoning = false;
     }
 
     return {
-      id: this.chunkId || 'chatcmpl-unknown',
-      object: 'chat.completion.chunk',
+      id: this.chunkId || "chatcmpl-unknown",
+      object: "chat.completion.chunk",
       created: this.created,
       model: this.model,
       choices: [
@@ -241,15 +232,15 @@ export class StreamTransformer {
 
   private ensureChunkMetadata(itemId?: string) {
     if (!this.chunkId) {
-      this.chunkId = itemId ? this.convertItemId(itemId) : 'chatcmpl-unknown';
+      this.chunkId = itemId ? this.convertItemId(itemId) : "chatcmpl-unknown";
     }
   }
 
   private resetAfterCompletion() {
     this.isInReasoning = false;
     this.reasoningItemsWithOutput.clear();
-    this.chunkId = '';
-    this.model = 'gpt-5';
+    this.chunkId = "";
+    this.model = "gpt-5";
     this.created = Math.floor(Date.now() / 1000);
   }
 
@@ -258,9 +249,9 @@ export class StreamTransformer {
    */
   private convertId(id: string): string {
     if (!id) {
-      return 'chatcmpl-unknown';
+      return "chatcmpl-unknown";
     }
-    return id.replace('resp_', 'chatcmpl-');
+    return id.replace("resp_", "chatcmpl-");
   }
 
   /**
@@ -269,10 +260,10 @@ export class StreamTransformer {
    */
   private convertItemId(itemId: string): string {
     if (!itemId) {
-      return 'chatcmpl-unknown';
+      return "chatcmpl-unknown";
     }
     // 提取 msg_ 或 rs_ 后面的部分
-    const id = itemId.replace(/^(msg_|rs_)/, 'chatcmpl-');
+    const id = itemId.replace(/^(msg_|rs_)/, "chatcmpl-");
     return id;
   }
 }

+ 12 - 12
src/app/v1/_lib/codex/types/compatible.ts

@@ -31,12 +31,12 @@ export interface ChatCompletionRequest {
 }
 
 export interface ReasoningConfig {
-  effort?: 'minimal' | 'low' | 'medium' | 'high';
-  summary?: 'auto' | 'concise' | 'detailed';
+  effort?: "minimal" | "low" | "medium" | "high";
+  summary?: "auto" | "concise" | "detailed";
 }
 
 export interface ChatCompletionTool {
-  type: 'function';
+  type: "function";
   function: {
     name: string;
     description?: string;
@@ -46,34 +46,34 @@ export interface ChatCompletionTool {
 }
 
 export interface ToolChoiceObject {
-  type: 'function';
+  type: "function";
   function?: {
     name: string;
   };
 }
 
 export interface ChatMessage {
-  role: 'system' | 'user' | 'assistant';
+  role: "system" | "user" | "assistant";
   content: string | ContentPart[];
   name?: string;
 }
 
 export interface ContentPart {
-  type: 'text' | 'image_url';
+  type: "text" | "image_url";
   text?: string;
   image_url?: ImageURL;
 }
 
 export interface ImageURL {
   url: string;
-  detail?: 'low' | 'high' | 'auto';
+  detail?: "low" | "high" | "auto";
 }
 
 // ============ 响应类型(非流式)============
 
 export interface ChatCompletionResponse {
   id: string;
-  object: 'chat.completion';
+  object: "chat.completion";
   created: number;
   model: string;
   choices: ChatCompletionChoice[];
@@ -88,7 +88,7 @@ export interface ChatCompletionResponse {
 export interface ChatCompletionChoice {
   index: number;
   message: ChatMessage;
-  finish_reason: 'stop' | 'length' | 'content_filter' | 'tool_calls' | null;
+  finish_reason: "stop" | "length" | "content_filter" | "tool_calls" | null;
   logprobs?: null;
 }
 
@@ -96,7 +96,7 @@ export interface ChatCompletionChoice {
 
 export interface ChatCompletionChunk {
   id: string;
-  object: 'chat.completion.chunk';
+  object: "chat.completion.chunk";
   created: number;
   model: string;
   choices: ChunkChoice[];
@@ -106,9 +106,9 @@ export interface ChatCompletionChunk {
 export interface ChunkChoice {
   index: number;
   delta: {
-    role?: 'assistant';
+    role?: "assistant";
     content?: string;
   };
-  finish_reason: 'stop' | 'length' | null;
+  finish_reason: "stop" | "length" | null;
   logprobs?: null;
 }

+ 36 - 36
src/app/v1/_lib/codex/types/response.ts

@@ -20,25 +20,25 @@ export interface ResponseRequest {
   tool_choice?: string | ToolChoiceObject;
   tools?: ResponseTool[];
   top_p?: number;
-  truncation?: 'auto' | 'disabled';
+  truncation?: "auto" | "disabled";
   user?: string;
   service_tier?: string;
 }
 
 export interface ReasoningConfig {
-  effort?: 'minimal' | 'low' | 'medium' | 'high';
-  summary?: 'auto' | 'concise' | 'detailed';
+  effort?: "minimal" | "low" | "medium" | "high";
+  summary?: "auto" | "concise" | "detailed";
 }
 
 export interface ToolChoiceObject {
-  type: 'function';
+  type: "function";
   function?: {
     name: string;
   };
 }
 
 export interface ResponseTool {
-  type: 'function';
+  type: "function";
   function: {
     name: string;
     description?: string;
@@ -50,12 +50,12 @@ export interface ResponseTool {
 export type InputItem = MessageInput | ToolOutputsInput;
 
 export interface MessageInput {
-  role: 'user' | 'assistant' | 'developer';
+  role: "user" | "assistant" | "developer";
   content: ContentItem[];
 }
 
 export interface ToolOutputsInput {
-  type: 'tool_outputs';
+  type: "tool_outputs";
   outputs: ToolOutput[];
 }
 
@@ -67,12 +67,12 @@ export interface ToolOutput {
 export type ContentItem = TextContent | ImageContent;
 
 export interface TextContent {
-  type: 'input_text' | 'output_text';
+  type: "input_text" | "output_text";
   text: string;
 }
 
 export interface ImageContent {
-  type: 'input_image';
+  type: "input_image";
   image_url: string;
 }
 
@@ -80,10 +80,10 @@ export interface ImageContent {
 
 export interface ResponseObject {
   id: string;
-  object: 'response';
+  object: "response";
   created: number;
   model: string;
-  status: 'completed' | 'failed' | 'incomplete';
+  status: "completed" | "failed" | "incomplete";
   output: OutputItem[];
   usage: {
     input_tokens: number;
@@ -100,27 +100,27 @@ export type OutputItem = ReasoningOutput | MessageOutput | ToolCallsOutput;
 
 export interface ReasoningOutput {
   id: string;
-  type: 'reasoning';
+  type: "reasoning";
   summary?: SummaryText[];
 }
 
 export interface MessageOutput {
   id: string;
-  type: 'message';
-  role: 'assistant';
-  status: 'completed';
+  type: "message";
+  role: "assistant";
+  status: "completed";
   content: OutputContent[];
 }
 
 export interface ToolCallsOutput {
   id: string;
-  type: 'tool_calls';
+  type: "tool_calls";
   tool_calls: ToolCall[];
 }
 
 export interface ToolCall {
   id: string;
-  type: 'function';
+  type: "function";
   function: {
     name: string;
     arguments: string;
@@ -128,28 +128,28 @@ export interface ToolCall {
 }
 
 export interface OutputContent {
-  type: 'output_text';
+  type: "output_text";
   text: string;
   annotations?: unknown[];
   logprobs?: unknown[];
 }
 
 export interface SummaryText {
-  type: 'summary_text';
+  type: "summary_text";
   text: string;
 }
 
 // ============ SSE 事件类型 ============
 
 export type SSEEventType =
-  | 'response.created'
-  | 'response.output_text.delta'
-  | 'response.reasoning_summary_text.delta'
-  | 'response.reasoning_summary_text.done'
-  | 'response.reasoning_summary_part.done'
-  | 'response.completed'
-  | 'response.failed'
-  | 'error';
+  | "response.created"
+  | "response.output_text.delta"
+  | "response.reasoning_summary_text.delta"
+  | "response.reasoning_summary_text.done"
+  | "response.reasoning_summary_part.done"
+  | "response.completed"
+  | "response.failed"
+  | "error";
 
 export interface SSEEvent {
   event: SSEEventType;
@@ -158,20 +158,20 @@ export interface SSEEvent {
 
 export interface ResponseCreatedEvent {
   id: string;
-  object: 'response';
+  object: "response";
   created: number;
   model: string;
-  status: 'generating';
+  status: "generating";
 }
 
 export interface OutputTextDeltaEvent {
-  type: 'response.output_text.delta';
+  type: "response.output_text.delta";
   item_id: string;
   delta: string;
 }
 
 export interface ReasoningSummaryTextDeltaEvent {
-  type: 'response.reasoning_summary_text.delta';
+  type: "response.reasoning_summary_text.delta";
   item_id: string;
   delta: string;
   output_index: number;
@@ -181,7 +181,7 @@ export interface ReasoningSummaryTextDeltaEvent {
 }
 
 export interface ReasoningSummaryTextDoneEvent {
-  type: 'response.reasoning_summary_text.done';
+  type: "response.reasoning_summary_text.done";
   item_id: string;
   text: string;
   output_index: number;
@@ -190,7 +190,7 @@ export interface ReasoningSummaryTextDoneEvent {
 }
 
 export interface ReasoningSummaryPartDoneEvent {
-  type: 'response.reasoning_summary_part.done';
+  type: "response.reasoning_summary_part.done";
   item_id: string;
   part: SummaryText;
   output_index: number;
@@ -200,10 +200,10 @@ export interface ReasoningSummaryPartDoneEvent {
 
 export interface ResponseCompletedEvent {
   id: string;
-  object: 'response';
+  object: "response";
   created: number;
   model: string;
-  status: 'completed';
+  status: "completed";
   output: OutputItem[];
-  usage: ResponseObject['usage'];
+  usage: ResponseObject["usage"];
 }

+ 13 - 14
src/app/v1/_lib/headers.ts

@@ -20,19 +20,19 @@ export class HeaderProcessor {
   constructor(config: HeaderProcessorConfig = {}) {
     // 初始化黑名单(默认包含代理相关的 headers)
     const defaultBlacklist = [
-      'x-forwarded-for',
-      'x-forwarded-host', 
-      'x-forwarded-port',
-      'x-forwarded-proto',
+      "x-forwarded-for",
+      "x-forwarded-host",
+      "x-forwarded-port",
+      "x-forwarded-proto",
     ];
-    
+
     // 如果不保留 authorization,添加到黑名单
     if (!config.preserveAuthorization) {
-      defaultBlacklist.push('authorization');
+      defaultBlacklist.push("authorization");
     }
-    
+
     this.blacklist = new Set(
-      [...defaultBlacklist, ...(config.blacklist || [])].map(h => h.toLowerCase())
+      [...defaultBlacklist, ...(config.blacklist || [])].map((h) => h.toLowerCase())
     );
 
     // 初始化覆盖规则
@@ -50,12 +50,12 @@ export class HeaderProcessor {
     // 第一步:根据黑名单过滤,默认全部透传
     headers.forEach((value, key) => {
       const lowerKey = key.toLowerCase();
-      
+
       // 检查黑名单
       if (this.blacklist.has(lowerKey)) {
         return; // 跳过黑名单 header
       }
-      
+
       // 保留这个 header
       processed.set(key, value);
     });
@@ -76,9 +76,9 @@ export class HeaderProcessor {
       const url = new URL(baseUrl);
       return url.host;
     } catch (error) {
-      console.error("提取 host 失败:", error);
+      logger.error('提取 host 失败:', error);
       const match = baseUrl.match(/^https?:\/\/([^\/]+)/);
-      return match ? match[1] : 'localhost';
+      return match ? match[1] : "localhost";
     }
   }
 
@@ -89,8 +89,7 @@ export class HeaderProcessor {
     // 默认的代理配置:删除常见的转发相关 headers
     return new HeaderProcessor({
       preserveAuthorization: false,
-      ...config
+      ...config,
     });
   }
 }
-

+ 2 - 1
src/app/v1/_lib/proxy-handler.ts

@@ -1,4 +1,5 @@
 import type { Context } from "hono";
+import { logger } from '@/lib/logger';
 import { ProxySession } from "./proxy/session";
 import { ProxyAuthenticator } from "./proxy/auth-guard";
 import { ProxySessionGuard } from "./proxy/session-guard";
@@ -54,7 +55,7 @@ export async function handleProxyRequest(c: Context): Promise<Response> {
     const response = await ProxyForwarder.send(session);
     return await ProxyResponseHandler.dispatch(session, response);
   } catch (error) {
-    console.error("Proxy handler error:", error);
+    logger.error('Proxy handler error:', error);
     return await ProxyErrorHandler.handle(session, error);
   }
 }

+ 2 - 1
src/app/v1/_lib/proxy/error-handler.ts

@@ -1,4 +1,5 @@
 import { updateMessageRequestDuration, updateMessageRequestDetails } from "@/repository/message";
+import { logger } from '@/lib/logger';
 import { ProxyLogger } from "./logger";
 import { ProxyResponses } from "./responses";
 import { ProxyError } from "./errors";
@@ -29,7 +30,7 @@ export class ProxyErrorHandler {
       await updateMessageRequestDetails(session.messageContext.id, {
         errorMessage: errorMessage,
         providerChain: session.getProviderChain(),
-        statusCode: statusCode
+        statusCode: statusCode,
       });
 
       // 记录请求结束

+ 18 - 16
src/app/v1/_lib/proxy/errors.ts

@@ -11,14 +11,14 @@ export class ProxyError extends Error {
     message: string,
     public readonly statusCode: number,
     public readonly upstreamError?: {
-      body: string;          // 原始响应体(智能截断)
-      parsed?: unknown;      // 解析后的 JSON(如果有)
+      body: string; // 原始响应体(智能截断)
+      parsed?: unknown; // 解析后的 JSON(如果有)
       providerId?: number;
       providerName?: string;
     }
   ) {
     super(message);
-    this.name = 'ProxyError';
+    this.name = "ProxyError";
   }
 
   /**
@@ -34,8 +34,8 @@ export class ProxyError extends Error {
     response: Response,
     provider: { id: number; name: string }
   ): Promise<ProxyError> {
-    const contentType = response.headers.get('content-type') || '';
-    let body = '';
+    const contentType = response.headers.get("content-type") || "";
+    let body = "";
     let parsed: unknown = undefined;
 
     // 1. 读取响应体
@@ -46,7 +46,7 @@ export class ProxyError extends Error {
     }
 
     // 2. 尝试解析 JSON
-    if (contentType.includes('application/json') && body) {
+    if (contentType.includes("application/json") && body) {
       try {
         parsed = JSON.parse(body);
       } catch {
@@ -66,7 +66,7 @@ export class ProxyError extends Error {
       body: truncatedBody,
       parsed,
       providerId: provider.id,
-      providerName: provider.name
+      providerName: provider.name,
     });
   }
 
@@ -78,32 +78,32 @@ export class ProxyError extends Error {
    * - Generic: { "message": "..." } 或 { "error": "..." }
    */
   private static extractErrorMessage(parsed: unknown): string | null {
-    if (!parsed || typeof parsed !== 'object') return null;
+    if (!parsed || typeof parsed !== "object") return null;
 
     const obj = parsed as Record<string, unknown>;
 
     // Claude/OpenAI 格式:{ "error": { "message": "..." } }
-    if (obj.error && typeof obj.error === 'object') {
+    if (obj.error && typeof obj.error === "object") {
       const errorObj = obj.error as Record<string, unknown>;
 
       // Claude 格式:带 type
-      if (typeof errorObj.message === 'string' && typeof errorObj.type === 'string') {
+      if (typeof errorObj.message === "string" && typeof errorObj.type === "string") {
         return `${errorObj.type}: ${errorObj.message}`;
       }
 
       // OpenAI 格式:仅 message
-      if (typeof errorObj.message === 'string') {
+      if (typeof errorObj.message === "string") {
         return errorObj.message;
       }
     }
 
     // 通用格式:{ "message": "..." }
-    if (typeof obj.message === 'string') {
+    if (typeof obj.message === "string") {
       return obj.message;
     }
 
     // 简单格式:{ "error": "..." }
-    if (typeof obj.error === 'string') {
+    if (typeof obj.error === "string") {
       return obj.error;
     }
 
@@ -123,7 +123,7 @@ export class ProxyError extends Error {
 
     // 纯文本:截断到 500 字符
     if (body.length > 500) {
-      return body.substring(0, 500) + '...';
+      return body.substring(0, 500) + "...";
     }
 
     return body;
@@ -138,7 +138,9 @@ export class ProxyError extends Error {
 
     // Part 1: Provider 信息 + 状态码
     if (this.upstreamError?.providerName) {
-      parts.push(`Provider ${this.upstreamError.providerName} returned ${this.statusCode}: ${this.message}`);
+      parts.push(
+        `Provider ${this.upstreamError.providerName} returned ${this.statusCode}: ${this.message}`
+      );
     } else {
       parts.push(this.message);
     }
@@ -148,6 +150,6 @@ export class ProxyError extends Error {
       parts.push(`Upstream: ${this.upstreamError.body}`);
     }
 
-    return parts.join(' | ');
+    return parts.join(" | ");
   }
 }

+ 67 - 47
src/app/v1/_lib/proxy/forwarder.ts

@@ -4,6 +4,7 @@ import { recordFailure, recordSuccess, getCircuitState } from "@/lib/circuit-bre
 import { ProxyProviderResolver } from "./provider-selector";
 import { ProxyError } from "./errors";
 import { ModelRedirector } from "./model-redirector";
+import { logger } from "@/lib/logger";
 import type { ProxySession } from "./session";
 
 const MAX_RETRY_ATTEMPTS = 3;
@@ -17,7 +18,7 @@ export class ProxyForwarder {
     let lastError: Error | null = null;
     let attemptCount = 0;
     let currentProvider = session.provider;
-    const failedProviderIds: number[] = [];  // 记录已失败的供应商ID
+    const failedProviderIds: number[] = []; // 记录已失败的供应商ID
 
     // 智能重试循环
     while (attemptCount <= MAX_RETRY_ATTEMPTS) {
@@ -27,60 +28,68 @@ export class ProxyForwarder {
         // 成功:记录健康状态
         recordSuccess(currentProvider.id);
 
-        console.debug(`[ProxyForwarder] Request successful with provider ${currentProvider.id} (attempt ${attemptCount + 1})`);
+        logger.debug("ProxyForwarder: Request successful", {
+          providerId: currentProvider.id,
+          attempt: attemptCount + 1,
+        });
 
         return response;
-
       } catch (error) {
         attemptCount++;
         lastError = error as Error;
-        failedProviderIds.push(currentProvider.id);  // 记录失败的供应商
+        failedProviderIds.push(currentProvider.id); // 记录失败的供应商
 
         // 提取错误信息(支持 ProxyError 和普通 Error)
-        const errorMessage = error instanceof ProxyError
-          ? error.getDetailedErrorMessage()
-          : (error as Error).message;
+        const errorMessage =
+          error instanceof ProxyError ? error.getDetailedErrorMessage() : (error as Error).message;
 
         // 记录失败的供应商和错误信息到决策链
         session.addProviderToChain(currentProvider, {
-          reason: 'retry_attempt',
+          reason: "retry_attempt",
           circuitState: getCircuitState(currentProvider.id),
           attemptNumber: attemptCount,
-          errorMessage: errorMessage,  // 记录完整上游错误
+          errorMessage: errorMessage, // 记录完整上游错误
         });
 
         // 记录失败
         recordFailure(currentProvider.id, lastError);
 
-        console.warn(
-          `[ProxyForwarder] Provider ${currentProvider.id} failed (attempt ${attemptCount}/${MAX_RETRY_ATTEMPTS + 1}): ${lastError.message}`
-        );
+        logger.warn("ProxyForwarder: Provider failed", {
+          providerId: currentProvider.id,
+          attempt: attemptCount,
+          maxAttempts: MAX_RETRY_ATTEMPTS + 1,
+          error: lastError.message,
+        });
 
         // 如果还有重试机会,选择新的供应商
         if (attemptCount <= MAX_RETRY_ATTEMPTS) {
           const alternativeProvider = await ProxyForwarder.selectAlternative(
             session,
-            failedProviderIds  // 传入所有已失败的供应商ID列表
+            failedProviderIds // 传入所有已失败的供应商ID列表
           );
 
           if (!alternativeProvider) {
-            console.error(`[ProxyForwarder] No alternative provider available, stopping retries`);
+            logger.error("ProxyForwarder: No alternative provider available, stopping retries");
             break;
           }
 
           currentProvider = alternativeProvider;
           session.setProvider(currentProvider);
 
-          console.info(`[ProxyForwarder] Retry ${attemptCount}: Switched to provider ${currentProvider.id}`);
+          logger.info("ProxyForwarder: Switched to alternative provider", {
+            retryAttempt: attemptCount,
+            newProviderId: currentProvider.id,
+          });
         }
       }
     }
 
     // 所有重试都失败
     // 如果最后一个错误是 ProxyError,提取详细信息(包含上游响应)
-    const errorDetails = lastError instanceof ProxyError
-      ? lastError.getDetailedErrorMessage()
-      : (lastError?.message || 'Unknown error');
+    const errorDetails =
+      lastError instanceof ProxyError
+        ? lastError.getDetailedErrorMessage()
+        : lastError?.message || "Unknown error";
 
     throw new Error(
       `All providers failed after ${attemptCount} attempts. Last error: ${errorDetails}`
@@ -90,7 +99,10 @@ export class ProxyForwarder {
   /**
    * 实际转发请求
    */
-  private static async doForward(session: ProxySession, provider: typeof session.provider): Promise<Response> {
+  private static async doForward(
+    session: ProxySession,
+    provider: typeof session.provider
+  ): Promise<Response> {
     if (!provider) {
       throw new Error("Provider is required");
     }
@@ -98,17 +110,17 @@ export class ProxyForwarder {
     // 应用模型重定向(如果配置了)
     const wasRedirected = ModelRedirector.apply(session, provider);
     if (wasRedirected) {
-      console.debug(`[ProxyForwarder] Model redirected for provider ${provider.id}`);
+      logger.debug("ProxyForwarder: Model redirected", { providerId: provider.id });
     }
 
     const processedHeaders = ProxyForwarder.buildHeaders(session, provider);
 
     // 开发模式:输出最终请求头
-    if (process.env.NODE_ENV === 'development') {
-      console.debug(`[ProxyForwarder] Final request headers:`, {
+    if (process.env.NODE_ENV === "development") {
+      logger.trace("ProxyForwarder: Final request headers", {
         provider: provider.name,
         providerType: provider.providerType,
-        headers: Object.fromEntries(processedHeaders.entries())
+        headers: Object.fromEntries(processedHeaders.entries()),
       });
     }
 
@@ -116,16 +128,19 @@ export class ProxyForwarder {
     let forwardUrl = session.requestUrl;
 
     // OpenAI Compatible 请求:自动替换为 Response API 端点
-    if (session.originalFormat === 'openai') {
+    if (session.originalFormat === "openai") {
       forwardUrl = new URL(session.requestUrl);
-      forwardUrl.pathname = '/v1/responses';
-      console.debug(`[ProxyForwarder] Codex request: rewriting path ${session.requestUrl.pathname} → /v1/responses`);
+      forwardUrl.pathname = "/v1/responses";
+      logger.debug("ProxyForwarder: Codex request path rewrite", {
+        from: session.requestUrl.pathname,
+        to: "/v1/responses",
+      });
     }
 
     const proxyUrl = buildProxyUrl(provider.url, forwardUrl);
 
     // 输出最终代理 URL(用于调试)
-    console.debug(`[ProxyForwarder] Final proxy URL: ${proxyUrl}`);
+    logger.debug("ProxyForwarder: Final proxy URL", { url: proxyUrl });
 
     const hasBody = session.method !== "GET" && session.method !== "HEAD";
 
@@ -137,15 +152,15 @@ export class ProxyForwarder {
       requestBody = bodyString;
 
       // 调试日志:输出实际转发的请求体(仅在开发环境)
-      if (process.env.NODE_ENV === 'development') {
-        console.debug(`[ProxyForwarder] Forwarding request:`, {
+      if (process.env.NODE_ENV === "development") {
+        logger.trace("ProxyForwarder: Forwarding request", {
           provider: provider.name,
           providerId: provider.id,
           proxyUrl: proxyUrl,
           format: session.originalFormat,
           method: session.method,
           bodyLength: bodyString.length,
-          bodyPreview: bodyString.slice(0, 1000)
+          bodyPreview: bodyString.slice(0, 1000),
         });
       }
     }
@@ -153,7 +168,7 @@ export class ProxyForwarder {
     const init: RequestInit = {
       method: session.method,
       headers: processedHeaders,
-      ...(requestBody ? { body: requestBody } : {})
+      ...(requestBody ? { body: requestBody } : {}),
     };
 
     (init as Record<string, unknown>).verbose = true;
@@ -163,7 +178,8 @@ export class ProxyForwarder {
       response = await fetch(proxyUrl, init);
     } catch (fetchError) {
       // 捕获 fetch 原始错误(网络错误、DNS 解析失败、JSON 序列化错误等)
-      console.error(`[ProxyForwarder] Fetch failed for provider ${provider.id}:`, {
+      logger.error("ProxyForwarder: Fetch failed", {
+        providerId: provider.id,
         error: fetchError,
         errorType: fetchError?.constructor?.name,
         errorMessage: (fetchError as Error)?.message,
@@ -181,7 +197,7 @@ export class ProxyForwarder {
     if (!response.ok) {
       throw await ProxyError.fromUpstreamResponse(response, {
         id: provider.id,
-        name: provider.name
+        name: provider.name,
       });
     }
 
@@ -193,7 +209,7 @@ export class ProxyForwarder {
    */
   private static async selectAlternative(
     session: ProxySession,
-    excludeProviderIds: number[]  // 改为数组,排除所有失败的供应商
+    excludeProviderIds: number[] // 改为数组,排除所有失败的供应商
   ): Promise<typeof session.provider | null> {
     // 使用公开的选择方法,传入排除列表
     const alternativeProvider = await ProxyProviderResolver.pickRandomProviderWithExclusion(
@@ -202,44 +218,48 @@ export class ProxyForwarder {
     );
 
     if (!alternativeProvider) {
-      console.warn(
-        `[ProxyForwarder] No alternative provider available (excluded: ${excludeProviderIds.join(', ')})`
-      );
+      logger.warn("ProxyForwarder: No alternative provider available", {
+        excludedProviders: excludeProviderIds,
+      });
       return null;
     }
 
     // 确保不是已失败的供应商之一
     if (excludeProviderIds.includes(alternativeProvider.id)) {
-      console.error(
-        `[ProxyForwarder] Selector returned excluded provider ${alternativeProvider.id}, this should not happen`
-      );
+      logger.error("ProxyForwarder: Selector returned excluded provider", {
+        providerId: alternativeProvider.id,
+        message: "This should not happen",
+      });
       return null;
     }
 
     return alternativeProvider;
   }
 
-  private static buildHeaders(session: ProxySession, provider: NonNullable<typeof session.provider>): Headers {
+  private static buildHeaders(
+    session: ProxySession,
+    provider: NonNullable<typeof session.provider>
+  ): Headers {
     const outboundKey = provider.key;
 
     // 构建请求头覆盖规则
     const overrides: Record<string, string> = {
-      "host": HeaderProcessor.extractHost(provider.url),
-      "authorization": `Bearer ${outboundKey}`,
+      host: HeaderProcessor.extractHost(provider.url),
+      authorization: `Bearer ${outboundKey}`,
       "x-api-key": outboundKey,
-      "content-type": "application/json"  // 确保 Content-Type
+      "content-type": "application/json", // 确保 Content-Type
     };
 
     // Codex 特殊处理:强制设置 User-Agent
     // Codex 供应商检测 User-Agent,只接受 codex_cli_rs 客户端
-    if (provider.providerType === 'codex') {
+    if (provider.providerType === "codex") {
       overrides["user-agent"] = "codex_cli_rs/1.0.0 (Mac OS 14.0.0; arm64)";
-      console.debug(`[ProxyForwarder] Codex provider detected, forcing User-Agent: codex_cli_rs/1.0.0`);
+      logger.debug("ProxyForwarder: Codex provider detected, forcing User-Agent");
     }
 
     const headerProcessor = HeaderProcessor.createForProxy({
       blacklist: [],
-      overrides
+      overrides,
     });
 
     return headerProcessor.process(session.headers);

+ 13 - 4
src/app/v1/_lib/proxy/logger.ts

@@ -5,7 +5,11 @@ import type { Provider } from "@/types/provider";
 import type { ProxySession } from "./session";
 
 export class ProxyLogger {
-  static async logNonStream(session: ProxySession, provider: Provider, responseBody: string): Promise<void> {
+  static async logNonStream(
+    session: ProxySession,
+    provider: Provider,
+    responseBody: string
+  ): Promise<void> {
     if (!isDevelopment()) {
       return;
     }
@@ -17,7 +21,8 @@ export class ProxyLogger {
 
     const requestLogHeader = session.request.note ? `${session.request.note}\n` : "";
     const requestLogBody = session.request.log || "(empty)";
-    const logContent = `=== Non-Stream API Call ${fileName} ===\n` +
+    const logContent =
+      `=== Non-Stream API Call ${fileName} ===\n` +
       `User: ${session.userName}\n` +
       `Provider: ${provider.name} (${provider.url})\n` +
       `Timestamp: ${timestamp.toISOString()}\n\n` +
@@ -54,7 +59,8 @@ export class ProxyLogger {
       ? `${session.provider.name}${session.provider.url ? ` (${session.provider.url})` : ""}`
       : "unavailable";
     const requestLogHeader = session.request.note ? `${session.request.note}\n` : "";
-    const logContent = `=== Proxy Failure ${fileName} ===\n` +
+    const logContent =
+      `=== Proxy Failure ${fileName} ===\n` +
       `Timestamp: ${timestamp.toISOString()}\n` +
       `Request: ${session.method} ${session.requestUrl.toString()}\n` +
       `User: ${session.userName}\n` +
@@ -77,7 +83,10 @@ export class ProxyLogger {
   }
 
   private static buildFileName(timestamp: Date, suffix: "nonstream" | "failure"): string {
-    const dateStr = String(timestamp.getMonth() + 1).padStart(2, "0") + "-" + String(timestamp.getDate()).padStart(2, "0");
+    const dateStr =
+      String(timestamp.getMonth() + 1).padStart(2, "0") +
+      "-" +
+      String(timestamp.getDate()).padStart(2, "0");
     const timeStr = [timestamp.getHours(), timestamp.getMinutes(), timestamp.getSeconds()]
       .map((value) => String(value).padStart(2, "0"))
       .join("-");

+ 10 - 3
src/app/v1/_lib/proxy/message-service.ts

@@ -6,7 +6,14 @@ export class ProxyMessageService {
     const authState = session.authState;
     const provider = session.provider;
 
-    if (!authState || !authState.success || !authState.user || !authState.key || !authState.apiKey || !provider) {
+    if (
+      !authState ||
+      !authState.success ||
+      !authState.user ||
+      !authState.key ||
+      !authState.apiKey ||
+      !provider
+    ) {
       session.setMessageContext(null);
       return;
     }
@@ -16,14 +23,14 @@ export class ProxyMessageService {
       user_id: authState.user.id,
       key: authState.apiKey,
       model: session.request.model ?? undefined,
-      session_id: session.sessionId ?? undefined,  // 新增:传入 session_id
+      session_id: session.sessionId ?? undefined, // 新增:传入 session_id
     });
 
     session.setMessageContext({
       id: messageRequest.id,
       user: authState.user,
       key: authState.key,
-      apiKey: authState.apiKey
+      apiKey: authState.apiKey,
     });
   }
 }

+ 4 - 8
src/app/v1/_lib/proxy/model-redirector.ts

@@ -1,4 +1,5 @@
 import type { Provider } from "@/types/provider";
+import { logger } from '@/lib/logger';
 import type { ProxySession } from "./session";
 
 /**
@@ -8,7 +9,6 @@ import type { ProxySession } from "./session";
  * 例如:将 "gpt-5" 重定向为 "gpt-5-codex"
  */
 export class ModelRedirector {
-
   /**
    * 应用模型重定向(如果配置了)
    *
@@ -25,7 +25,7 @@ export class ModelRedirector {
     // 获取原始模型名称
     const originalModel = session.request.model;
     if (!originalModel) {
-      console.debug('[ModelRedirector] No model found in request, skipping redirect');
+      logger.debug('[ModelRedirector] No model found in request, skipping redirect');
       return false;
     }
 
@@ -55,7 +55,7 @@ export class ModelRedirector {
     session.request.buffer = encoder.encode(updatedBody).buffer;
 
     // 更新日志(记录重定向)
-    session.request.note = `[Model Redirected: ${originalModel} → ${redirectedModel}] ${session.request.note || ''}`;
+    session.request.note = `[Model Redirected: ${originalModel} → ${redirectedModel}] ${session.request.note || ""}`;
 
     return true;
   }
@@ -83,10 +83,6 @@ export class ModelRedirector {
    * @returns 是否配置了重定向
    */
   static hasRedirect(model: string, provider: Provider): boolean {
-    return !!(
-      provider.modelRedirects &&
-      model &&
-      provider.modelRedirects[model]
-    );
+    return !!(provider.modelRedirects && model && provider.modelRedirects[model]);
   }
 }

+ 98 - 58
src/app/v1/_lib/proxy/provider-selector.ts

@@ -5,23 +5,29 @@ import { SessionManager } from "@/lib/session-manager";
 import { isCircuitOpen, getCircuitState } from "@/lib/circuit-breaker";
 import { ProxyLogger } from "./logger";
 import { ProxyResponses } from "./responses";
+import { logger } from "@/lib/logger";
 import type { ProxySession } from "./session";
 
 export class ProxyProviderResolver {
-  static async ensure(session: ProxySession, targetProviderType: 'claude' | 'codex' = 'claude'): Promise<Response | null> {
+  static async ensure(
+    session: ProxySession,
+    targetProviderType: "claude" | "codex" = "claude"
+  ): Promise<Response | null> {
     // 标记选择方法
-    let selectionMethod: 'reuse' | 'random' | 'group_filter' | 'fallback' = 'random';
+    let selectionMethod: "reuse" | "random" | "group_filter" | "fallback" = "random";
 
     // 尝试复用之前的供应商(基于 session)
     const reusedProvider = await ProxyProviderResolver.findReusable(session, targetProviderType);
     if (reusedProvider) {
       session.setProvider(reusedProvider);
-      selectionMethod = 'reuse';
+      selectionMethod = "reuse";
     }
 
     // 如果没有可复用的,随机选择
     if (!session.provider) {
-      session.setProvider(await ProxyProviderResolver.pickRandomProvider(session, [], targetProviderType));
+      session.setProvider(
+        await ProxyProviderResolver.pickRandomProvider(session, [], targetProviderType)
+      );
     }
 
     // 选定供应商后,进行原子性并发检查并追踪
@@ -37,22 +43,30 @@ export class ProxyProviderResolver {
 
       if (!checkResult.allowed) {
         // 并发限制失败
-        console.warn(
-          `[ProviderSelector] Provider ${session.provider.name} concurrent session limit exceeded (${checkResult.count}/${limit})`
-        );
+        logger.warn("ProviderSelector: Provider concurrent session limit exceeded", {
+          providerName: session.provider.name,
+          providerId: session.provider.id,
+          current: checkResult.count,
+          limit,
+        });
 
         // 记录失败
-        await ProxyLogger.logFailure(session, new Error(checkResult.reason || 'Session limit exceeded'));
-        return ProxyResponses.buildError(503, checkResult.reason || '供应商并发限制已达到');
+        await ProxyLogger.logFailure(
+          session,
+          new Error(checkResult.reason || "Session limit exceeded")
+        );
+        return ProxyResponses.buildError(503, checkResult.reason || "供应商并发限制已达到");
       }
 
-      console.debug(
-        `[ProviderSelector] ✅ Session tracked atomically: ${session.sessionId} → ${session.provider.name} (count=${checkResult.count})`
-      );
+      logger.debug("ProviderSelector: Session tracked atomically", {
+        sessionId: session.sessionId,
+        providerName: session.provider.name,
+        count: checkResult.count,
+      });
 
       // 记录到决策链
       session.addProviderToChain(session.provider, {
-        reason: 'initial_selection',
+        reason: "initial_selection",
         selectionMethod,
         circuitState: getCircuitState(session.provider.id),
       });
@@ -65,7 +79,7 @@ export class ProxyProviderResolver {
         providerId: session.provider.id,
         providerName: session.provider.name,
       }).catch((error) => {
-        console.error('[ProviderSelector] Failed to update session provider info:', error);
+        logger.error("ProviderSelector: Failed to update session provider info", { error });
       });
 
       return null;
@@ -74,7 +88,7 @@ export class ProxyProviderResolver {
     // 无可用供应商
     const status = 503;
     const message = "暂无可用的上游服务";
-    console.error("[ProviderSelector] No available providers");
+    logger.error("ProviderSelector: No available providers");
     await ProxyLogger.logFailure(session, new Error(message));
     return ProxyResponses.buildError(status, message);
   }
@@ -88,14 +102,17 @@ export class ProxyProviderResolver {
     excludeIds: number[]
   ): Promise<Provider | null> {
     // 从 session 读取供应商类型,避免参数传递和类型不一致
-    const targetProviderType = session.providerType || 'claude';
+    const targetProviderType = session.providerType || "claude";
     return this.pickRandomProvider(session, excludeIds, targetProviderType);
   }
 
   /**
    * 查找可复用的供应商(基于 session)
    */
-  private static async findReusable(session: ProxySession, targetProviderType: 'claude' | 'codex'): Promise<Provider | null> {
+  private static async findReusable(
+    session: ProxySession,
+    targetProviderType: "claude" | "codex"
+  ): Promise<Provider | null> {
     if (!session.shouldReuseProvider() || !session.sessionId) {
       return null;
     }
@@ -103,31 +120,44 @@ export class ProxyProviderResolver {
     // 从 Redis 读取该 session 绑定的 provider
     const providerId = await SessionManager.getSessionProvider(session.sessionId);
     if (!providerId) {
-      console.debug(`[ProviderSelector] Session ${session.sessionId} has no bound provider`);
+      logger.debug("ProviderSelector: Session has no bound provider", {
+        sessionId: session.sessionId,
+      });
       return null;
     }
 
     // 验证 provider 可用性
     const provider = await findProviderById(providerId);
     if (!provider || !provider.isEnabled) {
-      console.debug(`[ProviderSelector] Session ${session.sessionId} provider ${providerId} unavailable`);
+      logger.debug("ProviderSelector: Session provider unavailable", {
+        sessionId: session.sessionId,
+        providerId,
+      });
       return null;
     }
 
     // 检查供应商类型是否匹配
     if (provider.providerType !== targetProviderType) {
-      console.debug(`[ProviderSelector] Provider ${provider.id} type mismatch: ${provider.providerType} !== ${targetProviderType}`);
+      logger.debug("ProviderSelector: Provider type mismatch", {
+        providerId: provider.id,
+        actual: provider.providerType,
+        expected: targetProviderType,
+      });
       return null;
     }
 
-    console.info(`[ProviderSelector] Reusing provider ${provider.name} (id=${provider.id}) for session ${session.sessionId}`);
+    logger.info("ProviderSelector: Reusing provider", {
+      providerName: provider.name,
+      providerId: provider.id,
+      sessionId: session.sessionId,
+    });
     return provider;
   }
 
   private static async pickRandomProvider(
     session?: ProxySession,
-    excludeIds: number[] = [],  // 排除已失败的供应商
-    targetProviderType: 'claude' | 'codex' = 'claude'  // 目标供应商类型
+    excludeIds: number[] = [], // 排除已失败的供应商
+    targetProviderType: "claude" | "codex" = "claude" // 目标供应商类型
   ): Promise<Provider | null> {
     const allProviders = await findProviderList();
 
@@ -140,7 +170,7 @@ export class ProxyProviderResolver {
     );
 
     if (enabledProviders.length === 0) {
-      console.warn('[ProviderSelector] No enabled providers after exclusion filter');
+      logger.warn("ProviderSelector: No enabled providers after exclusion filter");
       return null;
     }
 
@@ -148,19 +178,18 @@ export class ProxyProviderResolver {
     let candidateProviders = enabledProviders;
     const userGroup = session?.authState?.user?.providerGroup;
     if (userGroup) {
-      const groupFiltered = enabledProviders.filter(
-        (p) => p.groupTag === userGroup
-      );
+      const groupFiltered = enabledProviders.filter((p) => p.groupTag === userGroup);
 
       if (groupFiltered.length > 0) {
         candidateProviders = groupFiltered;
-        console.debug(
-          `[ProviderSelector] User group '${userGroup}' filter: ${groupFiltered.length} providers`
-        );
+        logger.debug("ProviderSelector: User group filter applied", {
+          userGroup,
+          count: groupFiltered.length,
+        });
       } else {
-        console.warn(
-          `[ProviderSelector] User group '${userGroup}' has no providers, falling back to all`
-        );
+        logger.warn("ProviderSelector: User group has no providers, falling back", {
+          userGroup,
+        });
       }
     }
 
@@ -169,11 +198,11 @@ export class ProxyProviderResolver {
 
     // 记录过滤掉的供应商(熔断或限流)
     const filteredOut = candidateProviders.filter(
-      p => !healthyProviders.find(hp => hp.id === p.id)
+      (p) => !healthyProviders.find((hp) => hp.id === p.id)
     );
     if (filteredOut.length > 0) {
       const reasons = await Promise.all(
-        filteredOut.map(async p => {
+        filteredOut.map(async (p) => {
           if (isCircuitOpen(p.id)) {
             const state = getCircuitState(p.id);
             return `${p.name}(id=${p.id}, circuit=${state})`;
@@ -181,11 +210,11 @@ export class ProxyProviderResolver {
           return `${p.name}(id=${p.id}, rate-limited)`;
         })
       );
-      console.debug(`[ProviderSelector] Filtered out: ${reasons.join(', ')}`);
+      logger.debug("ProviderSelector: Filtered out providers", { providers: reasons });
     }
 
     if (healthyProviders.length === 0) {
-      console.warn('[ProviderSelector] All providers rate limited, falling back to random');
+      logger.warn("ProviderSelector: All providers rate limited, falling back to random");
       // Fail Open:降级到随机选择(让上游拒绝)
       return this.weightedRandom(candidateProviders);
     }
@@ -197,20 +226,32 @@ export class ProxyProviderResolver {
     const selected = this.selectOptimal(topPriorityProviders);
 
     // 详细的选择日志
-    const minPriority = Math.min(...healthyProviders.map(p => p.priority || 0));
-    console.info(`[ProviderSelector] Selection Decision:
-  ├─ Target provider type: ${targetProviderType}
-  ├─ Total providers: ${allProviders.length}
-  ├─ Enabled (type-filtered): ${enabledProviders.length}
-  ├─ Excluded IDs: ${excludeIds.length > 0 ? excludeIds.join(', ') : 'none'}
-  ├─ User group filter: '${userGroup || 'none'}'
-  ├─ After group filter: ${candidateProviders.length} (${candidateProviders.map(p => p.name).join(', ')})
-  ├─ After health/circuit filter: ${healthyProviders.length}
-  ${filteredOut.length > 0 ? `│  └─ Filtered: ${filteredOut.map(p => p.name).join(', ')}` : ''}
-  ├─ Top priority level: ${minPriority}
-  ├─ Top priority candidates: ${topPriorityProviders.map(p => `${p.name}(w=${p.weight}, cost=${p.costMultiplier}x)`).join(', ')}
-  └─ ✓ Selected: ${selected.name} (id=${selected.id}, type=${selected.providerType}, priority=${selected.priority}, weight=${selected.weight}, cost=${selected.costMultiplier}x, circuit=${getCircuitState(selected.id)})
-    `);
+    const minPriority = Math.min(...healthyProviders.map((p) => p.priority || 0));
+    logger.info("ProviderSelector: Selection decision", {
+      targetProviderType,
+      totalProviders: allProviders.length,
+      enabledCount: enabledProviders.length,
+      excludedIds: excludeIds,
+      userGroup: userGroup || "none",
+      afterGroupFilter: candidateProviders.map((p) => p.name),
+      afterHealthFilter: healthyProviders.length,
+      filteredOut: filteredOut.map((p) => p.name),
+      topPriorityLevel: minPriority,
+      topPriorityCandidates: topPriorityProviders.map((p) => ({
+        name: p.name,
+        weight: p.weight,
+        cost: p.costMultiplier,
+      })),
+      selected: {
+        name: selected.name,
+        id: selected.id,
+        type: selected.providerType,
+        priority: selected.priority,
+        weight: selected.weight,
+        cost: selected.costMultiplier,
+        circuitState: getCircuitState(selected.id),
+      },
+    });
 
     return selected;
   }
@@ -226,19 +267,19 @@ export class ProxyProviderResolver {
       providers.map(async (p) => {
         // 0. 检查熔断器状态
         if (isCircuitOpen(p.id)) {
-          console.debug(`[ProviderSelector] Provider ${p.id} circuit breaker is open`);
+          logger.debug("ProviderSelector: Provider circuit breaker is open", { providerId: p.id });
           return null;
         }
 
         // 1. 检查金额限制
-        const costCheck = await RateLimitService.checkCostLimits(p.id, 'provider', {
+        const costCheck = await RateLimitService.checkCostLimits(p.id, "provider", {
           limit_5h_usd: p.limit5hUsd,
           limit_weekly_usd: p.limitWeeklyUsd,
           limit_monthly_usd: p.limitMonthlyUsd,
         });
 
         if (!costCheck.allowed) {
-          console.debug(`[ProviderSelector] Provider ${p.id} cost limit exceeded`);
+          logger.debug("ProviderSelector: Provider cost limit exceeded", { providerId: p.id });
           return null;
         }
 
@@ -260,10 +301,10 @@ export class ProxyProviderResolver {
     }
 
     // 找到最小的优先级值(最高优先级)
-    const minPriority = Math.min(...providers.map(p => p.priority || 0));
+    const minPriority = Math.min(...providers.map((p) => p.priority || 0));
 
     // 只返回该优先级的供应商
-    return providers.filter(p => (p.priority || 0) === minPriority);
+    return providers.filter((p) => (p.priority || 0) === minPriority);
   }
 
   /**
@@ -271,7 +312,7 @@ export class ProxyProviderResolver {
    */
   private static selectOptimal(providers: Provider[]): Provider {
     if (providers.length === 0) {
-      throw new Error('No providers available for selection');
+      throw new Error("No providers available for selection");
     }
 
     if (providers.length === 1) {
@@ -313,4 +354,3 @@ export class ProxyProviderResolver {
     return providers[providers.length - 1];
   }
 }
-

+ 12 - 11
src/app/v1/_lib/proxy/rate-limit-guard.ts

@@ -1,5 +1,6 @@
-import type { ProxySession } from './session';
-import { RateLimitService } from '@/lib/rate-limit';
+import type { ProxySession } from "./session";
+import { logger } from '@/lib/logger';
+import { RateLimitService } from "@/lib/rate-limit";
 
 export class ProxyRateLimitGuard {
   /**
@@ -10,25 +11,25 @@ export class ProxyRateLimitGuard {
     if (!key) return null;
 
     // 1. 检查金额限制
-    const costCheck = await RateLimitService.checkCostLimits(key.id, 'key', {
+    const costCheck = await RateLimitService.checkCostLimits(key.id, "key", {
       limit_5h_usd: key.limit5hUsd,
       limit_weekly_usd: key.limitWeeklyUsd,
       limit_monthly_usd: key.limitMonthlyUsd,
     });
 
     if (!costCheck.allowed) {
-      return this.buildRateLimitResponse(key.id, 'key', costCheck.reason!);
+      return this.buildRateLimitResponse(key.id, "key", costCheck.reason!);
     }
 
     // 2. 检查并发 Session 限制
     const sessionCheck = await RateLimitService.checkSessionLimit(
       key.id,
-      'key',
+      "key",
       key.limitConcurrentSessions || 0
     );
 
     if (!sessionCheck.allowed) {
-      return this.buildRateLimitResponse(key.id, 'key', sessionCheck.reason!);
+      return this.buildRateLimitResponse(key.id, "key", sessionCheck.reason!);
     }
 
     return null;
@@ -39,19 +40,19 @@ export class ProxyRateLimitGuard {
    */
   private static buildRateLimitResponse(
     id: number,
-    type: 'key' | 'provider',
+    type: "key" | "provider",
     reason: string
   ): Response {
     const headers = new Headers({
-      'Content-Type': 'application/json',
-      'X-RateLimit-Type': type,
-      'Retry-After': '3600', // 1 小时后重试
+      "Content-Type": "application/json",
+      "X-RateLimit-Type": type,
+      "Retry-After": "3600", // 1 小时后重试
     });
 
     return new Response(
       JSON.stringify({
         error: {
-          type: 'rate_limit_error',
+          type: "rate_limit_error",
           message: reason,
         },
       }),

+ 51 - 34
src/app/v1/_lib/proxy/response-handler.ts

@@ -1,5 +1,10 @@
-import { updateMessageRequestDuration, updateMessageRequestCost, updateMessageRequestDetails } from "@/repository/message";
+import {
+  updateMessageRequestDuration,
+  updateMessageRequestCost,
+  updateMessageRequestDetails,
+} from "@/repository/message";
 import { findLatestPriceByModel } from "@/repository/model-price";
+import { logger } from '@/lib/logger';
 import { parseSSEData } from "@/lib/utils/sse";
 import { calculateRequestCost } from "@/lib/utils/cost-calculation";
 import { RateLimitService } from "@/lib/rate-limit";
@@ -31,7 +36,10 @@ export class ProxyResponseHandler {
     return await ProxyResponseHandler.handleStream(session, response);
   }
 
-  private static async handleNonStream(session: ProxySession, response: Response): Promise<Response> {
+  private static async handleNonStream(
+    session: ProxySession,
+    response: Response
+  ): Promise<Response> {
     const provider = session.provider;
     if (!provider) {
       return response;
@@ -41,7 +49,7 @@ export class ProxyResponseHandler {
     const statusCode = response.status;
 
     // 检查是否需要格式转换(OpenAI 请求 + Codex 供应商)
-    const needsTransform = session.originalFormat === 'openai' && session.providerType === 'codex';
+    const needsTransform = session.originalFormat === "openai" && session.providerType === "codex";
     let finalResponse = response;
 
     if (needsTransform) {
@@ -54,16 +62,16 @@ export class ProxyResponseHandler {
         // 转换为 OpenAI 格式
         const openAIResponse = ResponseTransformer.transform(responseData);
 
-        console.debug('[ResponseHandler] Transformed Response API → OpenAI format (non-stream)');
+        logger.debug('[ResponseHandler] Transformed Response API → OpenAI format (non-stream)');
 
         // 构建新的响应
         finalResponse = new Response(JSON.stringify(openAIResponse), {
           status: response.status,
           statusText: response.statusText,
-          headers: new Headers(response.headers)
+          headers: new Headers(response.headers),
         });
       } catch (error) {
-        console.error('[ResponseHandler] Failed to transform response:', error);
+        logger.error('[ResponseHandler] Failed to transform response:', error);
         // 转换失败时返回原始响应
         finalResponse = response;
       }
@@ -108,7 +116,11 @@ export class ProxyResponseHandler {
           if (session.request.model) {
             const priceData = await findLatestPriceByModel(session.request.model);
             if (priceData?.priceData) {
-              const cost = calculateRequestCost(usageMetrics, priceData.priceData, provider.costMultiplier);
+              const cost = calculateRequestCost(
+                usageMetrics,
+                priceData.priceData,
+                provider.costMultiplier
+              );
               if (cost.gt(0)) {
                 costUsdStr = cost.toString();
               }
@@ -121,10 +133,10 @@ export class ProxyResponseHandler {
             cacheCreationInputTokens: usageMetrics.cache_creation_input_tokens,
             cacheReadInputTokens: usageMetrics.cache_read_input_tokens,
             costUsd: costUsdStr,
-            status: statusCode >= 200 && statusCode < 300 ? 'completed' : 'error',
+            status: statusCode >= 200 && statusCode < 300 ? "completed" : "error",
             statusCode: statusCode,
           }).catch((error: unknown) => {
-            console.error('[ResponseHandler] Failed to update session usage:', error);
+            logger.error('[ResponseHandler] Failed to update session usage:', error);
           });
         }
 
@@ -139,7 +151,7 @@ export class ProxyResponseHandler {
             outputTokens: usageMetrics?.output_tokens,
             cacheCreationInputTokens: usageMetrics?.cache_creation_input_tokens,
             cacheReadInputTokens: usageMetrics?.cache_read_input_tokens,
-            providerChain: session.getProviderChain()
+            providerChain: session.getProviderChain(),
           });
 
           // 记录请求结束
@@ -149,7 +161,7 @@ export class ProxyResponseHandler {
 
         await ProxyLogger.logNonStream(session, provider, responseLogContent);
       } catch (error) {
-        console.error("Failed to handle non-stream log:", error);
+        logger.error('Failed to handle non-stream log:', error);
       }
     })();
 
@@ -165,11 +177,11 @@ export class ProxyResponseHandler {
     }
 
     // 检查是否需要格式转换(OpenAI 请求 + Codex 供应商)
-    const needsTransform = session.originalFormat === 'openai' && session.providerType === 'codex';
+    const needsTransform = session.originalFormat === "openai" && session.providerType === "codex";
     let processedStream: ReadableStream<Uint8Array> = response.body;
 
     if (needsTransform) {
-      console.debug('[ResponseHandler] Transforming Response API → OpenAI format (stream)');
+      logger.debug('[ResponseHandler] Transforming Response API → OpenAI format (stream)');
 
       // 创建转换流
       const streamTransformer = new StreamTransformer();
@@ -180,13 +192,13 @@ export class ProxyResponseHandler {
             const text = decoder.decode(chunk, { stream: true });
 
             // 解析并转换 SSE 事件
-            const lines = text.split('\n');
+            const lines = text.split("\n");
             for (const line of lines) {
-              if (line.startsWith('data: ')) {
+              if (line.startsWith("data: ")) {
                 const dataStr = line.slice(6).trim();
-                if (dataStr === '[DONE]') {
+                if (dataStr === "[DONE]") {
                   // 结束事件
-                  controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
+                  controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
                 } else {
                   try {
                     const event = JSON.parse(dataStr);
@@ -210,17 +222,17 @@ export class ProxyResponseHandler {
                     // 忽略解析错误的行
                   }
                 }
-              } else if (line.trim() === '') {
+              } else if (line.trim() === "") {
                 // 保留空行(SSE 分隔符)
-                controller.enqueue(new TextEncoder().encode('\n'));
+                controller.enqueue(new TextEncoder().encode("\n"));
               }
             }
           } catch (error) {
-            console.error('[ResponseHandler] Stream transform error:', error);
+            logger.error('[ResponseHandler] Stream transform error:', error);
             // 出错时传递原始 chunk
             controller.enqueue(chunk);
           }
-        }
+        },
       });
 
       processedStream = response.body.pipeThrough(transformStream) as ReadableStream<Uint8Array>;
@@ -262,7 +274,11 @@ export class ProxyResponseHandler {
         tracker.endRequest(messageContext.user.id, messageContext.id);
 
         for (const event of parsedEvents) {
-          if (event.event === "message_delta" && typeof event.data === "object" && event.data !== null) {
+          if (
+            event.event === "message_delta" &&
+            typeof event.data === "object" &&
+            event.data !== null
+          ) {
             const usageMetrics = extractUsageMetrics((event.data as Record<string, unknown>).usage);
             if (usageMetrics) {
               usageForCost = usageMetrics;
@@ -287,7 +303,11 @@ export class ProxyResponseHandler {
           if (session.request.model) {
             const priceData = await findLatestPriceByModel(session.request.model);
             if (priceData?.priceData) {
-              const cost = calculateRequestCost(usageForCost, priceData.priceData, provider.costMultiplier);
+              const cost = calculateRequestCost(
+                usageForCost,
+                priceData.priceData,
+                provider.costMultiplier
+              );
               if (cost.gt(0)) {
                 costUsdStr = cost.toString();
               }
@@ -300,10 +320,10 @@ export class ProxyResponseHandler {
             cacheCreationInputTokens: usageForCost.cache_creation_input_tokens,
             cacheReadInputTokens: usageForCost.cache_read_input_tokens,
             costUsd: costUsdStr,
-            status: statusCode >= 200 && statusCode < 300 ? 'completed' : 'error',
+            status: statusCode >= 200 && statusCode < 300 ? "completed" : "error",
             statusCode: statusCode,
           }).catch((error: unknown) => {
-            console.error('[ResponseHandler] Failed to update session usage:', error);
+            logger.error('[ResponseHandler] Failed to update session usage:', error);
           });
         }
 
@@ -314,10 +334,10 @@ export class ProxyResponseHandler {
           outputTokens: usageForCost?.output_tokens,
           cacheCreationInputTokens: usageForCost?.cache_creation_input_tokens,
           cacheReadInputTokens: usageForCost?.cache_read_input_tokens,
-          providerChain: session.getProviderChain()
+          providerChain: session.getProviderChain(),
         });
       } catch (error) {
-        console.error("Failed to save SSE content:", error);
+        logger.error('Failed to save SSE content:', error);
       } finally {
         reader.releaseLock();
       }
@@ -326,7 +346,7 @@ export class ProxyResponseHandler {
     return new Response(clientStream, {
       status: response.status,
       statusText: response.statusText,
-      headers: new Headers(response.headers)
+      headers: new Headers(response.headers),
     });
   }
 }
@@ -385,10 +405,7 @@ async function updateRequestCostFromUsage(
 /**
  * 追踪消费到 Redis(用于限流)
  */
-async function trackCostToRedis(
-  session: ProxySession,
-  usage: UsageMetrics | null
-): Promise<void> {
+async function trackCostToRedis(session: ProxySession, usage: UsageMetrics | null): Promise<void> {
   if (!usage || !session.sessionId) return;
 
   const messageContext = session.messageContext;
@@ -411,12 +428,12 @@ async function trackCostToRedis(
   await RateLimitService.trackCost(
     key.id,
     provider.id,
-    session.sessionId,  // 直接使用 session.sessionId
+    session.sessionId, // 直接使用 session.sessionId
     parseFloat(cost.toString())
   );
 
   // 刷新 session 时间戳(滑动窗口)
   void SessionTracker.refreshSession(session.sessionId, key.id, provider.id).catch((error) => {
-    console.error('[ResponseHandler] Failed to refresh session tracker:', error);
+    logger.error('[ResponseHandler] Failed to refresh session tracker:', error);
   });
 }

+ 4 - 4
src/app/v1/_lib/proxy/responses.ts

@@ -3,15 +3,15 @@ export class ProxyResponses {
     const payload = {
       error: {
         message,
-        type: String(status)
-      }
+        type: String(status),
+      },
     };
 
     return new Response(JSON.stringify(payload), {
       status,
       headers: {
-        "content-type": "application/json; charset=utf-8"
-      }
+        "content-type": "application/json; charset=utf-8",
+      },
     });
   }
 }

+ 10 - 13
src/app/v1/_lib/proxy/session-guard.ts

@@ -1,6 +1,7 @@
-import type { ProxySession } from './session';
-import { SessionManager } from '@/lib/session-manager';
-import { SessionTracker } from '@/lib/session-tracker';
+import type { ProxySession } from "./session";
+import { logger } from '@/lib/logger';
+import { SessionManager } from "@/lib/session-manager";
+import { SessionTracker } from "@/lib/session-tracker";
 
 /**
  * Session 守卫:负责为请求分配 Session ID
@@ -14,7 +15,7 @@ export class ProxySessionGuard {
   static async ensure(session: ProxySession): Promise<void> {
     const keyId = session.authState?.key?.id;
     if (!keyId) {
-      console.warn('[ProxySessionGuard] No key ID, skipping session assignment');
+      logger.warn('[ProxySessionGuard] No key ID, skipping session assignment');
       return;
     }
 
@@ -26,18 +27,14 @@ export class ProxySessionGuard {
       const messages = session.getMessages();
 
       // 3. 获取或创建 session_id
-      const sessionId = await SessionManager.getOrCreateSessionId(
-        keyId,
-        messages,
-        clientSessionId
-      );
+      const sessionId = await SessionManager.getOrCreateSessionId(keyId, messages, clientSessionId);
 
       // 4. 设置到 session 对象
       session.setSessionId(sessionId);
 
       // 5. 追踪 session(添加到活跃集合)
       void SessionTracker.trackSession(sessionId, keyId).catch((err) => {
-        console.error('[ProxySessionGuard] Failed to track session:', err);
+        logger.error('[ProxySessionGuard] Failed to track session:', err);
       });
 
       // 6. 存储 session 详细信息到 Redis(用于实时监控)
@@ -50,7 +47,7 @@ export class ProxySessionGuard {
               keyId: session.authState.key.id,
               keyName: session.authState.key.name,
               model: session.request.model,
-              apiType: session.originalFormat === 'openai' ? 'codex' : 'chat',
+              apiType: session.originalFormat === "openai" ? "codex" : "chat",
             });
 
             // 可选:存储 messages(受环境变量控制)
@@ -60,7 +57,7 @@ export class ProxySessionGuard {
             }
           }
         } catch (error) {
-          console.error('[ProxySessionGuard] Failed to store session info:', error);
+          logger.error('[ProxySessionGuard] Failed to store session info:', error);
         }
       })();
 
@@ -68,7 +65,7 @@ export class ProxySessionGuard {
         `[ProxySessionGuard] Session assigned: ${sessionId} (key=${keyId}, messagesLength=${session.getMessagesLength()}, clientProvided=${!!clientSessionId})`
       );
     } catch (error) {
-      console.error('[ProxySessionGuard] Failed to assign session:', error);
+      logger.error('[ProxySessionGuard] Failed to assign session:', error);
       // 降级:生成新 session(不阻塞请求)
       const fallbackSessionId = SessionManager.generateSessionId();
       session.setSessionId(fallbackSessionId);

+ 17 - 13
src/app/v1/_lib/proxy/session.ts

@@ -1,4 +1,5 @@
 import type { Context } from "hono";
+import { logger } from '@/lib/logger';
 import type { Provider } from "@/types/provider";
 import type { User } from "@/types/user";
 import type { Key } from "@/types/key";
@@ -49,8 +50,8 @@ export class ProxySession {
   sessionId: string | null;
 
   // Codex 支持:记录原始请求格式和供应商类型
-  originalFormat: 'response' | 'openai' | 'claude' = 'claude';
-  providerType: 'claude' | 'codex' | null = null;
+  originalFormat: "response" | "openai" | "claude" = "claude";
+  providerType: "claude" | "codex" | null = null;
 
   // 上游决策链(记录尝试的供应商列表)
   private providerChain: ProviderChainItem[];
@@ -90,7 +91,10 @@ export class ProxySession {
       buffer: bodyResult.requestBodyBuffer,
       log: bodyResult.requestBodyLog,
       note: bodyResult.requestBodyLogNote,
-      model: typeof bodyResult.requestMessage.model === "string" ? bodyResult.requestMessage.model : null
+      model:
+        typeof bodyResult.requestMessage.model === "string"
+          ? bodyResult.requestMessage.model
+          : null,
     };
 
     return new ProxySession({ startTime, method, requestUrl, headers, headerLog, request });
@@ -106,14 +110,14 @@ export class ProxySession {
   setProvider(provider: Provider | null): void {
     this.provider = provider;
     if (provider) {
-      this.providerType = (provider.providerType as 'claude' | 'codex') || 'claude';
+      this.providerType = (provider.providerType as "claude" | "codex") || "claude";
     }
   }
 
   /**
    * 设置原始请求格式(从路由层调用)
    */
-  setOriginalFormat(format: 'response' | 'openai' | 'claude'): void {
+  setOriginalFormat(format: "response" | "openai" | "claude"): void {
     this.originalFormat = format;
   }
 
@@ -159,11 +163,11 @@ export class ProxySession {
   addProviderToChain(
     provider: Provider,
     metadata?: {
-      reason?: 'initial_selection' | 'retry_attempt' | 'retry_fallback' | 'reuse';
-      selectionMethod?: 'reuse' | 'random' | 'group_filter' | 'fallback';
-      circuitState?: 'closed' | 'open' | 'half-open';
+      reason?: "initial_selection" | "retry_attempt" | "retry_fallback" | "reuse";
+      selectionMethod?: "reuse" | "random" | "group_filter" | "fallback";
+      circuitState?: "closed" | "open" | "half-open";
       attemptNumber?: number;
-      errorMessage?: string;  // 错误信息(失败时记录)
+      errorMessage?: string; // 错误信息(失败时记录)
     }
   ): void {
     const item: ProviderChainItem = {
@@ -179,7 +183,7 @@ export class ProxySession {
       circuitState: metadata?.circuitState,
       timestamp: Date.now(),
       attemptNumber: metadata?.attemptNumber,
-      errorMessage: metadata?.errorMessage,  // 记录错误信息
+      errorMessage: metadata?.errorMessage, // 记录错误信息
     };
 
     // 避免重复添加同一个供应商(除非是重试,即有 attemptNumber)
@@ -243,8 +247,8 @@ async function parseRequestBody(c: Context): Promise<RequestBodyResult> {
 
   try {
     const parsedMessage = JSON.parse(requestBodyText) as Record<string, unknown>;
-    requestMessage = parsedMessage;  // 保留原始数据用于业务逻辑
-    requestBodyLog = JSON.stringify(optimizeRequestMessage(parsedMessage), null, 2);  // 仅在日志中优化
+    requestMessage = parsedMessage; // 保留原始数据用于业务逻辑
+    requestBodyLog = JSON.stringify(optimizeRequestMessage(parsedMessage), null, 2); // 仅在日志中优化
   } catch {
     requestMessage = { raw: requestBodyText };
     requestBodyLog = requestBodyText;
@@ -255,6 +259,6 @@ async function parseRequestBody(c: Context): Promise<RequestBodyResult> {
     requestMessage,
     requestBodyLog,
     requestBodyLogNote,
-    requestBodyBuffer
+    requestBodyBuffer,
   };
 }

+ 6 - 7
src/app/v1/_lib/url.ts

@@ -8,23 +8,22 @@ export function buildProxyUrl(baseUrl: string, requestUrl: URL): string {
   try {
     // 解析baseUrl
     const baseUrlObj = new URL(baseUrl);
-    
+
     // 合并路径:baseUrl的路径 + 请求的路径
     // 确保路径拼接正确(处理斜杠)
-    const basePath = baseUrlObj.pathname.replace(/\/$/, ''); // 移除末尾斜杠
+    const basePath = baseUrlObj.pathname.replace(/\/$/, ""); // 移除末尾斜杠
     const requestPath = requestUrl.pathname; // 这已经包含 /v1/...
-    
+
     // 构建最终URL
     baseUrlObj.pathname = basePath + requestPath;
     // 保留原始请求的查询参数
     baseUrlObj.search = requestUrl.search;
-    
+
     return baseUrlObj.toString();
   } catch (error) {
-    console.error("URL构建失败:", error);
+    logger.error('URL构建失败:', error);
     // 降级到字符串拼接
-    const normalizedBaseUrl = baseUrl.replace(/\/$/, ''); 
+    const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
     return `${normalizedBaseUrl}${requestUrl.pathname}${requestUrl.search}`;
   }
 }
-

+ 5 - 12
src/components/customs/concurrent-sessions-card.tsx

@@ -12,7 +12,7 @@ const REFRESH_INTERVAL = 5000; // 5秒刷新一次
 async function fetchConcurrentSessions(): Promise<number> {
   const result = await getConcurrentSessions();
   if (!result.ok) {
-    throw new Error(result.error || '获取并发数失败');
+    throw new Error(result.error || "获取并发数失败");
   }
   return result.data;
 }
@@ -31,25 +31,18 @@ export function ConcurrentSessionsCard() {
   });
 
   const handleClick = () => {
-    router.push('/dashboard/sessions');
+    router.push("/dashboard/sessions");
   };
 
   return (
-    <Card
-      className="cursor-pointer hover:border-primary transition-colors"
-      onClick={handleClick}
-    >
+    <Card className="cursor-pointer hover:border-primary transition-colors" onClick={handleClick}>
       <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
-        <CardTitle className="text-sm font-medium">
-          当前并发
-        </CardTitle>
+        <CardTitle className="text-sm font-medium">当前并发</CardTitle>
         <Activity className="h-4 w-4 text-muted-foreground" />
       </CardHeader>
       <CardContent>
         <div className="text-2xl font-bold">{data}</div>
-        <p className="text-xs text-muted-foreground">
-          最近 5 分钟活跃 Session(点击查看详情)
-        </p>
+        <p className="text-xs text-muted-foreground">最近 5 分钟活跃 Session(点击查看详情)</p>
       </CardContent>
     </Card>
   );

+ 15 - 34
src/components/customs/version-checker.tsx

@@ -1,16 +1,10 @@
-'use client';
+"use client";
 
-import { useEffect, useState } from 'react';
-import { ExternalLink, RefreshCw } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
+import { useEffect, useState } from "react";
+import { ExternalLink, RefreshCw } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
 
 interface VersionInfo {
   current: string;
@@ -28,16 +22,16 @@ export function VersionChecker() {
   const checkVersion = async () => {
     setLoading(true);
     try {
-      const response = await fetch('/api/version');
+      const response = await fetch("/api/version");
       const data = await response.json();
       setVersionInfo(data);
     } catch (error) {
-      console.error('检查版本失败:', error);
+      console.error("检查版本失败:", error);
       setVersionInfo({
-        current: 'dev',
+        current: "dev",
         latest: null,
         hasUpdate: false,
-        error: '网络错误',
+        error: "网络错误",
       });
     } finally {
       setLoading(false);
@@ -96,7 +90,7 @@ export function VersionChecker() {
                 </p>
                 {versionInfo.publishedAt && (
                   <p className="mt-1 text-xs text-muted-foreground">
-                    发布于 {new Date(versionInfo.publishedAt).toLocaleDateString('zh-CN')}
+                    发布于 {new Date(versionInfo.publishedAt).toLocaleDateString("zh-CN")}
                   </p>
                 )}
               </div>
@@ -111,26 +105,13 @@ export function VersionChecker() {
         )}
 
         <div className="flex gap-2">
-          <Button
-            variant="outline"
-            size="sm"
-            onClick={checkVersion}
-            disabled={loading}
-          >
-            <RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
+          <Button variant="outline" size="sm" onClick={checkVersion} disabled={loading}>
+            <RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
             检查更新
           </Button>
           {versionInfo?.releaseUrl && (
-            <Button
-              variant="outline"
-              size="sm"
-              asChild
-            >
-              <a
-                href={versionInfo.releaseUrl}
-                target="_blank"
-                rel="noopener noreferrer"
-              >
+            <Button variant="outline" size="sm" asChild>
+              <a href={versionInfo.releaseUrl} target="_blank" rel="noopener noreferrer">
                 <ExternalLink className="h-4 w-4" />
                 查看发布
               </a>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini