main 517e6cf77ed6 cached
146 files
496.8 KB
129.7k tokens
255 symbols
1 requests
Download .txt
Showing preview only (554K chars total). Download the full file or copy to clipboard to get everything.
Repository: ItusiAI/AI-Translation-Assistant-Pro
Branch: main
Commit: 517e6cf77ed6
Files: 146
Total size: 496.8 KB

Directory structure:
gitextract_k73jqg37/

├── .eslintrc.json
├── .gitignore
├── README.md
├── app/
│   ├── api/
│   │   ├── aliyun/
│   │   │   ├── oss/
│   │   │   │   ├── sts/
│   │   │   │   │   └── route.ts
│   │   │   │   └── upload/
│   │   │   │       └── route.ts
│   │   │   └── video-ocr/
│   │   │       ├── create/
│   │   │       │   └── route.ts
│   │   │       ├── query/
│   │   │       │   └── route.ts
│   │   │       └── status/
│   │   │           └── route.ts
│   │   ├── asr/
│   │   │   ├── aliyun/
│   │   │   │   └── recognize/
│   │   │   │       └── route.ts
│   │   │   ├── create/
│   │   │   │   └── route.ts
│   │   │   └── status/
│   │   │       └── route.ts
│   │   ├── auth/
│   │   │   ├── [...nextauth]/
│   │   │   │   ├── auth.ts
│   │   │   │   └── route.ts
│   │   │   └── register/
│   │   │       └── route.ts
│   │   ├── file/
│   │   │   └── extract/
│   │   │       └── route.ts
│   │   ├── ocr/
│   │   │   ├── kimi/
│   │   │   │   └── route.ts
│   │   │   ├── route.ts
│   │   │   └── step/
│   │   │       └── route.ts
│   │   ├── qwen/
│   │   │   ├── ocr/
│   │   │   │   └── route.ts
│   │   │   └── translate/
│   │   │       └── route.ts
│   │   ├── register/
│   │   │   └── route.ts
│   │   ├── subscription/
│   │   │   └── route.ts
│   │   ├── tencent/
│   │   │   └── ocr/
│   │   │       └── route.ts
│   │   ├── translate/
│   │   │   ├── claude/
│   │   │   │   └── route.ts
│   │   │   ├── kimi/
│   │   │   │   └── route.ts
│   │   │   ├── route.ts
│   │   │   ├── siliconflow/
│   │   │   │   └── route.ts
│   │   │   └── step/
│   │   │       └── route.ts
│   │   ├── upload/
│   │   │   └── route.ts
│   │   ├── user/
│   │   │   ├── info/
│   │   │   │   └── route.ts
│   │   │   ├── update/
│   │   │   │   └── route.ts
│   │   │   └── usage/
│   │   │       └── route.ts
│   │   └── webhook/
│   │       └── route.ts
│   ├── globals.css
│   ├── layout.tsx
│   ├── login/
│   │   ├── error.tsx
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── page.tsx
│   ├── pricing/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── profile/
│   │   └── page.tsx
│   ├── providers.tsx
│   ├── register/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── translate/
│       ├── layout.tsx
│       └── page.tsx
├── components/
│   ├── client-layout.tsx
│   ├── footer.tsx
│   ├── google-analytics.tsx
│   ├── header.tsx
│   ├── language-provider.tsx
│   ├── language-switcher.tsx
│   ├── language-toggle.tsx
│   ├── layout.tsx
│   ├── subscription-dialog.tsx
│   ├── testimonials.tsx
│   ├── theme-provider.tsx
│   ├── theme-toggle.tsx
│   └── ui/
│       ├── accordion.tsx
│       ├── alert-dialog.tsx
│       ├── alert.tsx
│       ├── aspect-ratio.tsx
│       ├── avatar.tsx
│       ├── badge.tsx
│       ├── breadcrumb.tsx
│       ├── button.tsx
│       ├── calendar.tsx
│       ├── card.tsx
│       ├── carousel.tsx
│       ├── chart.tsx
│       ├── checkbox.tsx
│       ├── collapsible.tsx
│       ├── command.tsx
│       ├── context-menu.tsx
│       ├── dialog.tsx
│       ├── drawer.tsx
│       ├── dropdown-menu.tsx
│       ├── form.tsx
│       ├── hover-card.tsx
│       ├── input-otp.tsx
│       ├── input.tsx
│       ├── label.tsx
│       ├── menubar.tsx
│       ├── navigation-menu.tsx
│       ├── pagination.tsx
│       ├── popover.tsx
│       ├── progress.tsx
│       ├── radio-group.tsx
│       ├── resizable.tsx
│       ├── scroll-area.tsx
│       ├── select.tsx
│       ├── separator.tsx
│       ├── sheet.tsx
│       ├── skeleton.tsx
│       ├── slider.tsx
│       ├── sonner.tsx
│       ├── switch.tsx
│       ├── table.tsx
│       ├── tabs.tsx
│       ├── textarea.tsx
│       ├── toast.tsx
│       ├── toaster.tsx
│       ├── toggle-group.tsx
│       ├── toggle.tsx
│       ├── tooltip.tsx
│       └── use-toast.ts
├── components.json
├── hooks/
│   └── use-toast.ts
├── lib/
│   ├── aliyun-oss-client.ts
│   ├── aliyun-oss-upload.ts
│   ├── aliyun-oss.ts
│   ├── aliyun-video-ocr.ts
│   ├── db/
│   │   └── migrate.ts
│   ├── deepseek.ts
│   ├── gemini.ts
│   ├── hooks/
│   │   ├── use-analytics.ts
│   │   └── use-quota.ts
│   ├── i18n/
│   │   ├── locales/
│   │   │   ├── en.json
│   │   │   └── zh.json
│   │   ├── translations.ts
│   │   └── use-translations.ts
│   ├── kimi.ts
│   ├── languages.ts
│   ├── qwen.ts
│   ├── server/
│   │   ├── tencent-sign.ts
│   │   └── translate.ts
│   ├── speech.ts
│   ├── step.ts
│   ├── stripe.ts
│   ├── tencent-asr.ts
│   ├── tencent-sign.ts
│   ├── tencent.ts
│   ├── utils.ts
│   └── zhipu.ts
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public/
│   ├── ads.txt
│   └── site.webmanifest
├── tailwind.config.js
├── tailwind.config.ts
├── tsconfig.json
└── types/
    ├── alicloud.d.ts
    └── next-auth.d.ts

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

================================================
FILE: .eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


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

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

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

# local env files
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: README.md
================================================
# AI 翻译助手

一个功能强大的 AI 驱动的多语言翻译和内容处理平台。

## 主要功能

### 1. 多模态翻译
- **文本翻译**:支持无限次免费文本翻译,多种语言互译
- **图片识别**:支持图片内容识别和翻译,可处理多种格式图片
- **PDF 处理**:支持 PDF 文档内容提取和翻译,优化的文本提取算法
- **语音识别**:支持语音内容识别和转换,多种语言支持
- **视频处理**:支持视频内容提取和字幕生成,自动时间轴对齐

### 2. 高级 AI 模型支持
- **多模型集成**:支持 GPT、Gemini、Kimi、Mistral 等多种 AI 模型
- **智能模型选择**:根据内容类型自动选择最佳模型处理
- **模型备份机制**:当主要模型失败时自动切换到备用模型
- **高质量翻译**:专业级翻译质量,保留原文格式和语义

### 3. 会员订阅系统
- **分级会员制**:支持试用版、月度会员和年度会员
- **差异化配额**:不同会员等级享有不同的服务配额
- **自动续期**:支持 Stripe 订阅自动续费
- **订阅管理**:支持查看订阅状态、到期时间等
- **订阅过期处理**:订阅到期后自动重置为试用版状态

### 4. 用户系统
- **账号管理**:支持邮箱注册和登录
- **社交登录**:支持 GitHub 和 Google 账号登录
- **个人资料**:支持修改用户名等基本信息
- **使用统计**:实时显示各项功能的使用情况

### 5. 配额管理
- **每日重置**:免费用户的使用配额每日0点自动重置
- **实时统计**:显示当日各项功能的剩余使用次数
- **配额升级**:付费会员可获得更多使用次数
  - **试用版**:
    - 无限文本翻译
    - 5次/日图片识别
    - 3次/日PDF处理
    - 2次/日语音识别
    - 1次/日视频处理
  - **月度会员**:
    - 无限文本翻译
    - 50次/日图片识别
    - 40次/日PDF处理
    - 30次/日语音识别
    - 10次/日视频处理
  - **年度会员**:
    - 无限文本翻译
    - 100次/日图片识别
    - 80次/日PDF处理
    - 60次/日语音识别
    - 20次/日视频处理

### 6. 性能优化
- **PDF处理优化**:
  - 多种文本提取方法,提高成功率
  - 备用处理机制,当OCR API失败时使用聊天API提取内容
  - 减少重试次数和等待时间,提升用户体验
- **响应式设计**:适配各种设备屏幕尺寸
- **多语言界面**:支持中英文界面切换
- **快速响应**:优化的API调用和缓存机制

## 技术栈

- **前端**:Next.js 14, React, TypeScript, Tailwind CSS
- **后端**:Node.js, PostgreSQL (Neon Serverless)
- **认证**:NextAuth.js
- **支付**:Stripe
- **国际化**:自定义i18n解决方案
- **云服务**:
  - 阿里云 OSS(文件存储)
  - 腾讯云(AI 服务)
  - 多种AI模型API集成

## 环境变量配置

项目运行需要配置以下环境变量:

```env
# 数据库配置
DATABASE_URL=

# 认证相关
NEXTAUTH_SECRET=
NEXTAUTH_URL=

# GitHub OAuth
GITHUB_ID=
GITHUB_SECRET=

# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Stripe 配置
STRIPE_SECRET_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID=
NEXT_PUBLIC_APP_URL=

# 阿里云配置
ALIYUN_ACCESS_KEY_ID=
ALIYUN_ACCESS_KEY_SECRET=
ALIYUN_OSS_BUCKET=
ALIYUN_OSS_REGION=
ALIYUN_RAM_ROLE_ARN=

# AI 模型 API Keys
NEXT_PUBLIC_GEMINI_API_KEY=
NEXT_PUBLIC_DEEPSEEK_API_KEY=
NEXT_PUBLIC_QWEN_API_KEY=
NEXT_PUBLIC_ZHIPU_API_KEY=
NEXT_PUBLIC_TENCENT_API_KEY=
NEXT_PUBLIC_KIMI_API_KEY=
NEXT_PUBLIC_OPENAI_API_KEY=
NEXT_PUBLIC_MINNIMAX_API_KEY=
NEXT_PUBLIC_SILICONFLOW_API_KEY=
NEXT_PUBLIC_OPENROUTER_API_KEY=
MISTRAL_API_KEY=
```

## 开发说明

1. 克隆项目
```bash
git clone [repository-url]
cd ai-translation-assistant-pro
```

2. 安装依赖
```bash
npm install
```

3. 配置环境变量
```bash
cp .env.example .env.local
# 编辑 .env.local 填入相应的值
```

4. 运行开发服务器
```bash
npm run dev
```

## 部署

项目可以部署到任何支持 Node.js 的平台。建议使用 Vercel 进行部署:

1. 在 Vercel 中导入项目
2. 配置环境变量
3. 部署完成后即可访问

## 许可证

[MIT License](LICENSE) 

================================================
FILE: app/api/aliyun/oss/sts/route.ts
================================================
import { NextResponse } from 'next/server'
import * as $OpenApi from '@alicloud/openapi-client'
import * as $STS20150401 from '@alicloud/sts20150401'

export async function GET() {
  try {
    if (!process.env.ALIYUN_ACCESS_KEY_ID || 
        !process.env.ALIYUN_ACCESS_KEY_SECRET || 
        !process.env.ALIYUN_RAM_ROLE_ARN || 
        !process.env.ALIYUN_OSS_BUCKET ||
        !process.env.ALIYUN_OSS_REGION) {
      throw new Error('缺少必要的环境变量配置');
    }

    // 打印 AccessKey 前缀,用于验证
    console.log('AccessKey 信息:', {
      accessKeyIdPrefix: process.env.ALIYUN_ACCESS_KEY_ID.substring(0, 8) + '****',
      region: process.env.ALIYUN_OSS_REGION,
      bucket: process.env.ALIYUN_OSS_BUCKET
    });

    console.log('RAM 角色信息:', {
      roleArn: process.env.ALIYUN_RAM_ROLE_ARN,
      accountId: process.env.ALIYUN_RAM_ROLE_ARN.split('::')[1]?.split(':')[0]
    });

    const config = new $OpenApi.Config({
      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
      endpoint: 'sts.aliyuncs.com',
      regionId: 'cn-hangzhou'  // 使用默认的 cn-hangzhou 地区
    });

    const client = new $STS20150401.default(config);

    // 获取 STS token,有效期15分钟
    const result = await client.assumeRole({
      roleArn: process.env.ALIYUN_RAM_ROLE_ARN,
      roleSessionName: 'video-upload',
      durationSeconds: 900
    });

    console.log('STS 响应成功');

    // 检查返回结果的结构
    if (!result.body || !result.body.credentials) {
      console.error('无效的 STS 响应结构:', result);
      throw new Error('无效的 STS 响应');
    }

    return NextResponse.json({
      success: true,
      data: {
        region: process.env.ALIYUN_OSS_REGION,
        bucket: process.env.ALIYUN_OSS_BUCKET,
        credentials: {
          accessKeyId: result.body.credentials.accessKeyId,
          accessKeySecret: result.body.credentials.accessKeySecret,
          securityToken: result.body.credentials.securityToken,
          expiration: result.body.credentials.expiration
        }
      }
    });
  } catch (error: any) {
    console.error('获取 STS token 失败:', error);
    console.error('详细错误信息:', {
      code: error.code,
      message: error.message,
      data: error.data,
      statusCode: error.statusCode,
      stack: error.stack
    });
    return NextResponse.json({
      success: false,
      message: `获取上传凭证失败: ${error.message || '未知错误'}`,
      error: {
        code: error.code,
        message: error.message,
        data: error.data,
        statusCode: error.statusCode
      }
    }, { status: 500 });
  }
} 

================================================
FILE: app/api/aliyun/oss/upload/route.ts
================================================
import { NextResponse } from 'next/server'
import OSS from 'ali-oss'
import { v4 as uuidv4 } from 'uuid'

export async function POST(request: Request) {
  try {
    const formData = await request.formData()
    const file = formData.get('file') as File
    
    if (!file) {
      return NextResponse.json(
        { message: '缺少文件' },
        { status: 400 }
      )
    }

    console.log('文件信息:', {
      name: file.name,
      type: file.type,
      size: file.size
    })

    // 创建 OSS 客户端
    const ossClient = new OSS({
      region: 'oss-cn-shanghai',
      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',
      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',
      bucket: process.env.ALIYUN_OSS_BUCKET || ''
    })

    try {
      // 将文件转换为 Buffer
      const arrayBuffer = await file.arrayBuffer()
      const buffer = Buffer.from(arrayBuffer)

      // 上传到 OSS
      const ext = file.name.split('.').pop()
      const fileName = `videos/${uuidv4()}.${ext}`
      console.log('开始上传文件到 OSS...')
      
      // 使用 Promise 包装 put 方法
      const uploadResult = await new Promise((resolve, reject) => {
        try {
          // @ts-ignore
          ossClient.put(fileName, buffer).then(result => {
            resolve(result)
          }).catch(err => {
            reject(err)
          })
        } catch (err) {
          reject(err)
        }
      })

      console.log('文件上传成功:', uploadResult)

      return NextResponse.json({
        success: true,
        url: (uploadResult as any).url
      })
    } catch (uploadError: any) {
      console.error('OSS上传错误:', {
        name: uploadError.name,
        message: uploadError.message,
        code: uploadError.code,
        requestId: uploadError.requestId,
        stack: uploadError.stack
      })
      throw new Error(`文件上传失败: ${uploadError.message}`)
    }

  } catch (error: any) {
    console.error('处理请求错误:', error)
    return NextResponse.json(
      { message: error.message || '文件上传失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/aliyun/video-ocr/create/route.ts
================================================
import { NextResponse } from 'next/server'
import RPCClient from '@alicloud/pop-core'

interface AsyncJobResult {
  RequestId: string
  Message: string
}

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { videoUrl } = body

    if (!videoUrl) {
      return NextResponse.json(
        { message: '缺少视频URL' },
        { status: 400 }
      )
    }

    // 创建视频识别客户端
    const client = new RPCClient({
      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',
      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',
      endpoint: 'https://videorecog.cn-shanghai.aliyuncs.com',
      apiVersion: '2020-03-20'
    })

    try {
      // 创建视频识别任务
      console.log('开始创建视频识别任务...')
      const params = {
        VideoUrl: videoUrl,
        Params: JSON.stringify([{
          Type: 'subtitles'
        }])
      }

      // 发送请求
      const result = await client.request<AsyncJobResult>('RecognizeVideoCastCrewList', params, {
        method: 'POST',
        formatParams: false,
        headers: {
          'content-type': 'application/json'
        }
      })
      
      console.log('创建任务结果:', result)

      if (!result.RequestId) {
        throw new Error('创建任务失败:未获取到任务ID')
      }

      return NextResponse.json({
        success: true,
        taskId: result.RequestId,
        message: result.Message
      })
    } catch (createError: any) {
      console.error('创建任务错误详情:', {
        name: createError.name,
        message: createError.message,
        code: createError.code,
        requestId: createError.RequestId,
        stack: createError.stack
      })
      throw new Error(`创建视频识别任务失败: ${createError.message}`)
    }

  } catch (error: any) {
    console.error('处理请求错误:', error)
    return NextResponse.json(
      { message: error.message || '创建视频识别任务失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/aliyun/video-ocr/query/route.ts
================================================
import { NextResponse } from 'next/server'
import RPCClient from '@alicloud/pop-core'

interface AsyncJobQueryResult {
  RequestId: string
  Data: {
    Status: string
    Result: string
    JobId: string
  }
}

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { taskId } = body

    if (!taskId) {
      return NextResponse.json(
        { message: '缺少任务ID' },
        { status: 400 }
      )
    }

    // 创建视频识别客户端
    const client = new RPCClient({
      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',
      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',
      endpoint: 'https://videorecog.cn-shanghai.aliyuncs.com',
      apiVersion: '2020-03-20'
    })

    try {
      // 查询任务结果
      console.log('开始查询任务结果, taskId:', taskId)
      const params = {
        JobId: taskId
      }

      // 发送请求
      const result = await client.request<AsyncJobQueryResult>('GetAsyncJobResult', params)
      
      console.log('原始查询结果:', JSON.stringify(result, null, 2))

      if (!result.Data) {
        console.log('未获取到Data字段:', result)
        throw new Error('查询任务失败:未获取到任务结果')
      }

      console.log('任务状态:', result.Data.Status)
      console.log('任务结果:', result.Data.Result)

      // 如果任务还在处理中,返回特定状态码
      if (result.Data.Status === 'PROCESS_RUNNING') {
        console.log('任务正在处理中...')
        return NextResponse.json({
          success: true,
          status: 'running',
          message: '任务正在处理中'
        }, { status: 202 })
      }

      // 如果任务失败,抛出错误
      if (result.Data.Status === 'PROCESS_FAILED') {
        console.log('任务处理失败')
        throw new Error('任务处理失败')
      }

      // 如果任务成功完成,返回结果
      if (result.Data.Status === 'PROCESS_SUCCESS') {
        console.log('任务处理成功,开始解析结果')
        let ocrResult = {}
        try {
          if (typeof result.Data.Result === 'string') {
            console.log('解析字符串结果')
            ocrResult = JSON.parse(result.Data.Result)
          } else {
            console.log('使用原始结果对象')
            ocrResult = result.Data.Result
          }
          console.log('解析后的结果:', ocrResult)
        } catch (e) {
          console.error('解析OCR结果失败:', e)
          console.log('使用原始结果')
          ocrResult = result.Data.Result
        }

        return NextResponse.json({
          success: true,
          status: 'success',
          data: ocrResult
        })
      }

      // 其他状态
      console.log('未知的任务状态:', result.Data.Status)
      return NextResponse.json({
        success: false,
        status: result.Data.Status,
        message: '未知的任务状态',
        data: result.Data
      })

    } catch (queryError: any) {
      console.error('查询任务错误详情:', {
        name: queryError.name,
        message: queryError.message,
        code: queryError.code,
        requestId: queryError.RequestId,
        stack: queryError.stack
      })
      throw new Error(`查询视频识别任务失败: ${queryError.message}`)
    }

  } catch (error: any) {
    console.error('处理请求错误:', error)
    return NextResponse.json(
      { message: error.message || '查询视频识别任务失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/aliyun/video-ocr/status/route.ts
================================================
import { NextResponse } from 'next/server'
import RPCClient from '@alicloud/pop-core'

interface VideoOCRResult {
  OcrResults?: Array<{
    DetailInfo: Array<{
      Text: string
      TimeStamp: number
    }>
    StartTime: number
    EndTime: number
  }>
  VideoOcrResults?: Array<{
    DetailInfo: Array<{
      Text: string
    }>
    StartTime: number
    EndTime: number
  }>
  SubtitlesResults?: Array<{
    SubtitlesAllResults?: Record<string, string>
    SubtitlesChineseResults?: Record<string, string>
    SubtitlesEnglishResults?: Record<string, string>
    SubtitlesAllResultsUrl?: string
    SubtitlesChineseResultsUrl?: string
    SubtitlesEnglishResultsUrl?: string
  }>
}

interface AsyncJobQueryResult {
  RequestId: string
  Data: {
    Status: string
    Result: string
    JobId: string
  }
}

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { taskId } = body

    if (!taskId) {
      return NextResponse.json(
        { message: '缺少任务ID' },
        { status: 400 }
      )
    }

    // 创建视频识别客户端
    const client = new RPCClient({
      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',
      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',
      endpoint: 'https://videorecog.cn-shanghai.aliyuncs.com',
      apiVersion: '2020-03-20',
      opts: {
        method: 'POST',
        timeout: 60000
      }
    })

    try {
      // 查询任务结果
      console.log('开始查询任务结果, taskId:', taskId)
      const params = {
        JobId: taskId
      }

      // 发送请求
      const result = await client.request<AsyncJobQueryResult>('GetAsyncJobResult', params, {
        method: 'POST',
        formatParams: true,
        headers: {
          'content-type': 'application/x-www-form-urlencoded'
        }
      })
      
      console.log('原始查询结果:', JSON.stringify(result, null, 2))

      // 检查结果格式
      if (!result || !result.Data) {
        console.log('未获取到Data字段:', result)
        throw new Error('查询任务失败:未获取到任务结果')
      }

      console.log('任务状态:', result.Data.Status)

      // 处理不同的任务状态
      switch (result.Data.Status) {
        case 'PROCESS_RUNNING':
          console.log('任务正在处理中...')
          return NextResponse.json({
            success: true,
            status: 'running',
            message: '任务正在处理中'
          }, { status: 202 })

        case 'PROCESS_FAILED':
          console.log('任务处理失败:', result.Data)
          return NextResponse.json({
            success: false,
            status: 'failed',
            message: '任务处理失败',
            data: result.Data
          }, { status: 500 })

        case 'PROCESS_SUCCESS':
          console.log('任务处理成功,开始解析结果')
          let ocrResult: VideoOCRResult | null = null
          
          try {
            // 尝试解析结果
            if (result.Data.Result) {
              console.log('解析字符串结果:', result.Data.Result)
              if (typeof result.Data.Result === 'string') {
                ocrResult = JSON.parse(result.Data.Result)
              } else {
                ocrResult = result.Data.Result as VideoOCRResult
              }
            }

            // 提取所有文本内容
            const textContents: string[] = []
            
            // 处理 OcrResults
            if (ocrResult?.OcrResults) {
              ocrResult.OcrResults.forEach(result => {
                result.DetailInfo.forEach(detail => {
                  if (detail.Text) {
                    textContents.push(detail.Text)
                  }
                })
              })
            }

            // 处理 VideoOcrResults
            if (ocrResult?.VideoOcrResults) {
              ocrResult.VideoOcrResults.forEach(result => {
                result.DetailInfo.forEach(detail => {
                  if (detail.Text) {
                    textContents.push(detail.Text)
                  }
                })
              })
            }

            // 处理字幕结果
            if (ocrResult?.SubtitlesResults?.[0]) {
              const subtitles = ocrResult.SubtitlesResults[0]
              if (subtitles.SubtitlesChineseResults) {
                Object.values(subtitles.SubtitlesChineseResults).forEach(text => {
                  textContents.push(text)
                })
              }
              return NextResponse.json({
                success: true,
                status: 'success',
                data: {
                  text: textContents.join('\n'),
                  subtitles: {
                    all: subtitles.SubtitlesAllResults,
                    chinese: subtitles.SubtitlesChineseResults,
                    english: subtitles.SubtitlesEnglishResults,
                    allUrl: subtitles.SubtitlesAllResultsUrl,
                    chineseUrl: subtitles.SubtitlesChineseResultsUrl,
                    englishUrl: subtitles.SubtitlesEnglishResultsUrl
                  },
                  raw: ocrResult
                }
              })
            }

            // 如果没有字幕结果,返回文本内容
            return NextResponse.json({
              success: true,
              status: 'success',
              data: {
                text: textContents.join('\n'),
                raw: ocrResult
              }
            })

          } catch (e) {
            console.error('解析OCR结果失败:', e)
            console.log('返回原始结果')
            return NextResponse.json({
              success: true,
              status: 'success',
              data: {
                text: result.Data.Result,
                raw: result.Data.Result
              }
            })
          }

        case 'PROCESS_PENDING':
          console.log('任务等待处理中...')
          return NextResponse.json({
            success: true,
            status: 'pending',
            message: '任务等待处理中'
          }, { status: 202 })

        default:
          console.log('收到未知任务状态:', result.Data.Status, '完整结果:', result.Data)
          return NextResponse.json({
            success: true,
            status: result.Data.Status,
            message: '任务状态未知,请继续轮询',
            data: result.Data
          }, { status: 202 })
      }

    } catch (queryError: any) {
      console.error('查询任务错误详情:', {
        name: queryError.name,
        message: queryError.message,
        code: queryError.code,
        requestId: queryError.RequestId,
        stack: queryError.stack
      })
      throw new Error(`查询视频识别任务失败: ${queryError.message}`)
    }

  } catch (error: any) {
    console.error('处理请求错误:', error)
    return NextResponse.json(
      { message: error.message || '查询视频识别任务失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/asr/aliyun/recognize/route.ts
================================================
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  try {
    const { audioUrl, appKey, token, taskId } = await request.json();

    // 调用阿里云语音识别 API
    const response = await fetch('https://nls-gateway.aliyuncs.com/stream/v1/asr', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-NLS-Token': token,
      },
      body: JSON.stringify({
        appkey: appKey,
        audio_url: audioUrl,
        format: 'wav',
        sample_rate: 16000,
        enable_intermediate_result: true,
        enable_punctuation_prediction: true,
        enable_inverse_text_normalization: true,
      }),
    });

    if (!response.ok) {
      throw new Error('阿里云 API 请求失败');
    }

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error: any) {
    return NextResponse.json(
      { error: error.message || '识别失败' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/asr/create/route.ts
================================================
import { NextResponse } from 'next/server';
import { sign } from '@/lib/server/tencent-sign';

const endpoint = 'asr.tencentcloudapi.com';
const service = 'asr';
const version = '2019-06-14';
const region = 'ap-guangzhou';
const action = 'CreateRecTask';

export async function POST(request: Request) {
  try {
    const { engineType, channelNum, resTextFormat, sourceType, data } = await request.json();

    const timestamp = Math.floor(Date.now() / 1000);
    const params = {
      EngineModelType: engineType,
      ChannelNum: channelNum,
      ResTextFormat: resTextFormat,
      SourceType: sourceType,
      Data: data,
    };

    const signature = sign({
      secretId: process.env.TENCENT_SECRET_ID || '',
      secretKey: process.env.TENCENT_SECRET_KEY || '',
      endpoint,
      service,
      version,
      region,
      action,
      timestamp,
      payload: params,
    });

    const response = await fetch(`https://${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': signature,
        'X-TC-Action': action,
        'X-TC-Version': version,
        'X-TC-Region': region,
        'X-TC-Timestamp': timestamp.toString(),
      },
      body: JSON.stringify(params),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.Response?.Error?.Message || 'API request failed');
    }

    const result = await response.json();
    return NextResponse.json(result);
  } catch (error: any) {
    console.error('创建识别任务失败:', error);
    return NextResponse.json(
      { error: error.message || '创建识别任务失败' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/asr/status/route.ts
================================================
import { NextResponse } from 'next/server';
import { sign } from '@/lib/server/tencent-sign';

const endpoint = 'asr.tencentcloudapi.com';
const service = 'asr';
const version = '2019-06-14';
const region = 'ap-guangzhou';
const action = 'DescribeTaskStatus';

export async function POST(request: Request) {
  try {
    const { taskId } = await request.json();

    const timestamp = Math.floor(Date.now() / 1000);
    const params = {
      TaskId: taskId,
    };

    const signature = sign({
      secretId: process.env.TENCENT_SECRET_ID || '',
      secretKey: process.env.TENCENT_SECRET_KEY || '',
      endpoint,
      service,
      version,
      region,
      action,
      timestamp,
      payload: params,
    });

    const response = await fetch(`https://${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': signature,
        'X-TC-Action': action,
        'X-TC-Version': version,
        'X-TC-Region': region,
        'X-TC-Timestamp': timestamp.toString(),
      },
      body: JSON.stringify(params),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.Response?.Error?.Message || 'API request failed');
    }

    const result = await response.json();
    return NextResponse.json(result);
  } catch (error: any) {
    console.error('查询任务状态失败:', error);
    return NextResponse.json(
      { error: error.message || '查询任务状态失败' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/auth/[...nextauth]/auth.ts
================================================
import { AuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import { neon } from '@neondatabase/serverless'
import bcrypt from 'bcryptjs'

const sql = neon(process.env.DATABASE_URL!)

export const authOptions: AuthOptions = {
  debug: process.env.NODE_ENV === 'development',
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: "邮箱", type: "email" },
        password: { label: "密码", type: "password" }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Please enter email and password')
        }

        try {
          const result = await sql`
            SELECT * FROM auth_users WHERE email = ${credentials.email}
          `
          const user = result[0]
          console.log('查询到的用户数据:', user)

          if (!user || !user.password_hash) {
            throw new Error('Invalid email or password')
          }

          const isValid = await bcrypt.compare(credentials.password, user.password_hash)
          console.log('密码比较结果:', isValid)
          
          if (!isValid) {
            throw new Error('Invalid email or password')
          }

          return {
            id: user.id,
            email: user.email,
            name: user.name || null
          }
        } catch (error) {
          console.error('登录验证错误:', error)
          throw new Error('Authentication server error, please try again later')
        }
      }
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email!,
          image: profile.picture,
        }
      },
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
      httpOptions: {
        timeout: 10000
      },
      profile(profile) {
        return {
          id: String(profile.id),
          name: profile.name || profile.login,
          email: profile.email!,
          image: profile.avatar_url,
        }
      },
    })
  ],
  pages: {
    signIn: '/login',
    error: '/login',
  },
  callbacks: {
    async signIn({ user, account }) {
      if (!user.email) return false

      try {
        // 检查用户是否存在
        const users = await sql`
          SELECT id, email FROM auth_users
          WHERE email = ${user.email}
        `

        // 如果是首次登录,创建新用户
        if (users.length === 0 && account?.providerAccountId) {
          if (account.provider === 'github') {
            await sql`
              INSERT INTO auth_users (
                email,
                name,
                github_id,
                text_quota,
                image_quota,
                pdf_quota,
                speech_quota,
                video_quota,
                created_at,
                updated_at
              ) VALUES (
                ${user.email},
                ${user.name},
                ${account.providerAccountId},
                -1,
                10,
                8,
                5,
                2,
                CURRENT_TIMESTAMP,
                CURRENT_TIMESTAMP
              )
            `
          } else {
            await sql`
              INSERT INTO auth_users (
                email,
                name,
                google_id,
                text_quota,
                image_quota,
                pdf_quota,
                speech_quota,
                video_quota,
                created_at,
                updated_at
              ) VALUES (
                ${user.email},
                ${user.name},
                ${account.providerAccountId},
                -1,
                10,
                8,
                5,
                2,
                CURRENT_TIMESTAMP,
                CURRENT_TIMESTAMP
              )
            `
          }
        }

        return true
      } catch (error) {
        console.error('Error in signIn callback:', error)
        return false
      }
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
      }
      return token
    },
    async session({ session }) {
      if (session.user?.email) {
        const users = await sql`
          SELECT id
          FROM auth_users
          WHERE email = ${session.user.email}
        `
        if (users.length > 0) {
          session.user.id = users[0].id
        }
      }
      return session
    }
  }
} 

================================================
FILE: app/api/auth/[...nextauth]/route.ts
================================================
import NextAuth from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/auth'

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST } 

================================================
FILE: app/api/auth/register/route.ts
================================================
import { NextResponse } from 'next/server'
import { neon } from '@neondatabase/serverless'
import bcrypt from 'bcryptjs'

export async function POST(req: Request) {
  const sql = neon(process.env.DATABASE_URL!)
  
  try {
    const { email, password, name } = await req.json()

    if (!email || !password) {
      return new NextResponse('Missing email or password', { status: 400 })
    }

    // 检查邮箱是否已被注册
    const existingUser = await sql`
      SELECT * FROM users WHERE email = ${email}
    `

    if (existingUser.length > 0) {
      return new NextResponse('Email already exists', { status: 400 })
    }

    // 密码加密
    const hashedPassword = await bcrypt.hash(password, 10)

    // 创建用户
    await sql`
      INSERT INTO users (email, password)
      VALUES (${email}, ${hashedPassword})
    `

    return new NextResponse('User created successfully', { status: 201 })
  } catch (error: any) {
    console.error('注册失败:', error)
    return new NextResponse('Internal Server Error', { status: 500 })
  }
} 

================================================
FILE: app/api/file/extract/route.ts
================================================
import { NextResponse } from 'next/server'
import { Mistral } from '@mistralai/mistralai';

const KIMI_API_KEY = process.env.NEXT_PUBLIC_KIMI_API_KEY
const KIMI_API_URL = 'https://api.moonshot.cn/v1'
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY

// 增加超时时间
const TIMEOUT = {
  UPLOAD: 20000,    // 20秒
  CONTENT: 30000,   // 30秒
  PROCESS: 45000    // 45秒
}

// 带超时的 fetch
async function fetchWithTimeout(url: string, options: RequestInit, timeout: number) {
  const controller = new AbortController()
  const id = setTimeout(() => controller.abort(), timeout)

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    })
    clearTimeout(id)
    
    // 检查响应状态
    if (!response.ok) {
      const error = await response.json().catch(() => ({ error: { message: '请求失败' } }))
      throw new Error(error.error?.message || `请求失败: ${response.status}`)
    }
    
    return response
  } catch (error: any) {
    clearTimeout(id)
    if (error.name === 'AbortError') {
      throw new Error('请求超时,请重试')
    }
    throw error
  }
}

// 分步上传文件
async function uploadFile(file: string, filename: string) {
  try {
    const formData = new FormData()
    const fileBlob = new Blob([Buffer.from(file, 'base64')], { type: 'application/pdf' })
    const pdfFile = new File([fileBlob], filename, { type: 'application/pdf' })
    formData.append('file', pdfFile)
    formData.append('purpose', 'file-extract')

    const uploadResponse = await fetchWithTimeout(`${KIMI_API_URL}/files`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${KIMI_API_KEY}`
      },
      body: formData
    }, TIMEOUT.UPLOAD)

    if (!uploadResponse.ok) {
      const error = await uploadResponse.json().catch(() => ({ error: { message: '文件上传失败' } }))
      console.error('KIMI文件上传错误:', error)
      throw new Error(error.error?.message || '文件上传失败')
    }

    return await uploadResponse.json()
  } catch (error: any) {
    if (error.name === 'AbortError') {
      throw new Error('文件上传超时')
    }
    throw error
  }
}

// 获取文件内容
async function getFileContent(fileId: string) {
  try {
    const contentResponse = await fetchWithTimeout(`${KIMI_API_URL}/files/${fileId}/content`, {
      headers: {
        'Authorization': `Bearer ${KIMI_API_KEY}`
      }
    }, TIMEOUT.CONTENT)

    if (!contentResponse.ok) {
      const error = await contentResponse.json().catch(() => ({ error: { message: '文件内容获取失败' } }))
      console.error('KIMI文件内容获取错误:', error)
      throw new Error(error.error?.message || '文件内容获取失败')
    }

    return await contentResponse.text()
  } catch (error: any) {
    if (error.name === 'AbortError') {
      throw new Error('文件内容获取超时')
    }
    throw error
  }
}

// 处理文件内容
async function processContent(content: string) {
  try {
    const messages = [
      {
        role: 'system',
        content: '你是 Kimi,由 Moonshot AI 提供的人工智能助手。请提取文件中的所有文字内容,保持原文的格式和换行,不需要总结或解释。'
      },
      {
        role: 'system',
        content
      },
      {
        role: 'user',
        content: '请直接返回文件的原始内容,保持格式,不要添加任何解释或总结。'
      }
    ]

    const chatResponse = await fetchWithTimeout(`${KIMI_API_URL}/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${KIMI_API_KEY}`
      },
      body: JSON.stringify({
        model: 'moonshot-v1-32k',
        messages,
        temperature: 0.3
      })
    }, TIMEOUT.PROCESS)

    if (!chatResponse.ok) {
      const error = await chatResponse.json().catch(() => ({ error: { message: 'API请求失败' } }))
      console.error('KIMI API错误:', error)
      throw new Error(error.error?.message || 'API请求失败')
    }

    const data = await chatResponse.json()
    if (!data.choices?.[0]?.message?.content) {
      console.error('KIMI API响应格式错误:', data)
      throw new Error('API返回格式错误')
    }

    return data.choices[0].message.content.trim()
  } catch (error: any) {
    if (error.name === 'AbortError') {
      throw new Error('内容处理超时')
    }
    throw error
  }
}

// 使用 Mistral OCR API 处理 PDF
async function processPdfWithMistral(file: string, filename: string) {
  try {
    console.log('开始使用 Mistral OCR 处理 PDF...')
    
    // 创建 Mistral 客户端
    const client = new Mistral({ apiKey: MISTRAL_API_KEY || '' });
    
    // 将 base64 转换为 Buffer
    const fileBuffer = Buffer.from(file, 'base64');
    
    // 上传文件到 Mistral
    console.log('上传文件到 Mistral...')
    let uploadData;
    try {
      uploadData = await client.files.upload({
        file: {
          fileName: filename,
          content: fileBuffer,
        },
        purpose: "ocr"
      });
      console.log('文件上传成功,ID:', uploadData.id);
    } catch (uploadError: any) {
      console.error('Mistral 文件上传错误:', uploadError);
      throw new Error(uploadError.message || 'Mistral 文件上传失败');
    }
    
    // 获取签名 URL
    console.log('获取签名 URL...');
    let signedUrlData;
    try {
      signedUrlData = await client.files.getSignedUrl({
        fileId: uploadData.id,
      });
      console.log('获取签名 URL 成功');
    } catch (signedUrlError: any) {
      console.error('获取签名 URL 错误:', signedUrlError);
      throw new Error(signedUrlError.message || '获取签名 URL 失败');
    }
    
    // 使用 OCR 处理文件
    console.log('开始 OCR 处理...');
    let ocrData: any;
    try {
      ocrData = await client.ocr.process({
        model: "mistral-ocr-latest",
        document: {
          type: "document_url",
          documentUrl: signedUrlData.url,
        }
      });
      console.log('OCR 处理完成,响应数据类型:', typeof ocrData);
      if (typeof ocrData === 'object' && ocrData !== null) {
        console.log('OCR 响应数据结构:', Object.keys(ocrData).join(', '));
        // 输出更详细的响应结构
        for (const key of Object.keys(ocrData)) {
          console.log(`OCR 响应字段 ${key} 类型:`, typeof ocrData[key]);
          if (key === 'pages' && Array.isArray(ocrData.pages)) {
            console.log('pages 数组长度:', ocrData.pages.length);
            if (ocrData.pages.length > 0) {
              console.log('第一页结构:', Object.keys(ocrData.pages[0]).join(', '));
            }
          }
        }
      }
      console.log('OCR 响应数据片段:', typeof ocrData === 'string' ? ocrData.substring(0, 500) : JSON.stringify(ocrData).substring(0, 500) + '...');
    } catch (ocrError: any) {
      console.error('Mistral OCR 错误:', ocrError);
      throw new Error(ocrError.message || 'OCR 处理失败');
    }
    
    // 提取文本内容
    let extractedText = '';
    
    // 检查响应格式并记录详细信息
    if (!ocrData) {
      console.error('Mistral OCR 返回空响应');
      throw new Error('OCR 返回空响应');
    }
    
    // 处理 Markdown 格式的响应
    if (typeof ocrData === 'string') {
      console.log('OCR 返回 Markdown 格式的文本');
      // 直接返回 Markdown 文本,不需要额外处理
      return ocrData.trim();
    }
    
    // 检查响应对象中的各种可能字段
    if (ocrData.markdown) {
      console.log('从 markdown 字段提取文本');
      return String(ocrData.markdown).trim();
    }
    
    // 检查各种可能的文本字段
    if (ocrData.text) {
      console.log('从 text 字段提取文本');
      return String(ocrData.text).trim();
    }
    
    if (ocrData.content) {
      console.log('从 content 字段提取文本');
      return typeof ocrData.content === 'string' ? ocrData.content.trim() : JSON.stringify(ocrData.content);
    }
    
    // 检查是否有结果字段
    if (ocrData.result) {
      console.log('从 result 字段提取文本');
      if (typeof ocrData.result === 'string') {
        return ocrData.result.trim();
      } else if (typeof ocrData.result === 'object' && ocrData.result !== null) {
        // 检查result对象中的可能字段
        if (ocrData.result.text) {
          return String(ocrData.result.text).trim();
        } else if (ocrData.result.content) {
          return typeof ocrData.result.content === 'string' ? ocrData.result.content.trim() : JSON.stringify(ocrData.result.content);
        } else {
          return JSON.stringify(ocrData.result);
        }
      }
    }
    
    // 检查 ocrData 是否有 pages 属性
    if (ocrData.pages) {
      console.log(`发现 pages 字段,包含 ${Array.isArray(ocrData.pages) ? ocrData.pages.length : '未知数量'} 页`);
      
      // 正常处理 pages 数组
      if (Array.isArray(ocrData.pages)) {
        console.log(`提取 ${ocrData.pages.length} 页的文本`);
        
        extractedText = ocrData.pages.map((page: any, index: number) => {
          if (!page) {
            console.log(`第 ${index + 1} 页为空`);
            return '';
          }
          
          // 首先检查markdown字段,这是Mistral OCR的主要输出格式
          if (page.markdown) {
            console.log(`从第 ${index + 1} 页的markdown字段提取文本`);
            return page.markdown;
          } else if (page.text) {
            return page.text;
          } else if (page.content) {
            return typeof page.content === 'string' ? page.content : JSON.stringify(page.content);
          } else {
            console.log(`第 ${index + 1} 页没有文本内容:`, page);
            return '';
          }
        }).join('\n\n');
      } else {
        console.error('Mistral OCR 响应中 pages 不是数组:', ocrData.pages);
        // 尝试将 pages 作为文本返回
        if (typeof ocrData.pages === 'string') {
          return ocrData.pages.trim();
        } else {
          return JSON.stringify(ocrData.pages);
        }
      }
    } else {
      console.log('OCR 响应中没有找到 pages 字段,尝试从整个响应中提取文本');
      // 如果找不到任何已知字段,尝试将整个响应作为文本返回
      return JSON.stringify(ocrData);
    }
    
    console.log(`提取的文本长度: ${extractedText.length} 字符`);
    
    // 确保返回非空文本
    if (!extractedText || extractedText.trim() === '') {
      console.log('提取的文本为空,返回默认消息');
      return '无法从PDF中提取文本。这可能是因为PDF包含扫描图像或其他不可提取的内容。请尝试使用其他服务或上传不同的文件。';
    }
    
    return extractedText.trim();
  } catch (error: any) {
    console.error('Mistral OCR 处理错误:', error);
    if (error.name === 'AbortError') {
      throw new Error('Mistral OCR 处理超时');
    }
    throw error;
  }
}

export async function POST(request: Request) {
  try {
    const { file, filename, service } = await request.json()

    if (!file) {
      return NextResponse.json(
        { error: '未提供文件' },
        { status: 400 }
      )
    }

    // 检查文件大小
    const base64Size = file.length * 0.75 // base64到字节的近似转换
    if (base64Size > 5 * 1024 * 1024) { // 5MB
      return NextResponse.json(
        { error: '文件大小超过限制' },
        { status: 400 }
      )
    }

    if (service !== 'kimi' && service !== 'mistral') {
      return NextResponse.json(
        { error: '不支持的服务' },
        { status: 400 }
      )
    }

    if (service === 'kimi' && !KIMI_API_KEY) {
      return NextResponse.json(
        { error: '未配置Kimi API密钥' },
        { status: 500 }
      )
    }

    if (service === 'mistral' && !MISTRAL_API_KEY) {
      return NextResponse.json(
        { error: '未配置Mistral API密钥' },
        { status: 500 }
      )
    }

    // 分步处理
    try {
      let result = ''
      
      if (service === 'kimi') {
        // 使用 Kimi API 处理
        console.log('开始使用 Kimi API 处理...')
        // 步骤1:上传文件
        console.log('开始上传文件...')
        const fileObject = await uploadFile(file, filename)
        
        // 步骤2:获取文件内容
        console.log('开始获取文件内容...')
        const fileContent = await getFileContent(fileObject.id)
        
        // 步骤3:处理内容
        console.log('开始处理文件内容...')
        result = await processContent(fileContent)
      } else if (service === 'mistral') {
        // 使用 Mistral OCR API 处理
        console.log('开始使用 Mistral OCR API 处理...')
        result = await processPdfWithMistral(file, filename)
      }

      // 检查响应大小
      if (result.length > 5 * 1024 * 1024) { // 5MB
        throw new Error('响应内容过大')
      }

      return NextResponse.json({ text: result })
    } catch (error: any) {
      console.error('处理步骤错误:', error)
      if (error.name === 'AbortError' || error.message.includes('超时')) {
        return NextResponse.json(
          { error: '处理超时,请稍后重试' },
          { status: 503 }
        )
      }
      
      // 根据错误类型返回不同的状态码
      if (error.message.includes('文件大小超过限制') || error.message.includes('响应内容过大')) {
        return NextResponse.json(
          { error: error.message },
          { status: 413 }
        )
      }
      
      return NextResponse.json(
        { error: error.message || 'PDF处理失败' },
        { status: 500 }
      )
    }
  } catch (error: any) {
    console.error('PDF处理错误:', error)
    return NextResponse.json(
      { error: error.message || 'PDF处理失败' },
      { status: error.status || 500 }
    )
  }
}

================================================
FILE: app/api/ocr/kimi/route.ts
================================================
import { NextResponse } from 'next/server'
import OpenAI from 'openai'

const KIMI_API_KEY = process.env.NEXT_PUBLIC_KIMI_API_KEY
const KIMI_API_URL = 'https://api.moonshot.cn/v1'

export async function POST(request: Request) {
  try {
    const { image } = await request.json()
    
    if (!image) {
      return NextResponse.json(
        { error: 'No image data provided' },
        { status: 400 }
      )
    }

    if (!KIMI_API_KEY) {
      return NextResponse.json(
        { error: 'Kimi API key not found' },
        { status: 500 }
      )
    }

    // 从完整的 data URL 中提取 base64 数据
    const base64Data = image.split(';base64,').pop() || image;

    const openai = new OpenAI({
      apiKey: KIMI_API_KEY,
      baseURL: KIMI_API_URL
    })

    const response = await openai.chat.completions.create({
      model: 'moonshot-v1-32k-vision-preview',
      messages: [
        {
          role: 'system',
          content: '你是一个专业的图片文字识别助手。请提取图片中的所有文字,保持原有格式,不要添加任何解释。'
        },
        {
          role: 'user',
          content: [
            { type: 'text', text: '请提取这张图片中的所有文字:' },
            { type: 'image_url', image_url: { url: image } }
          ]
        }
      ],
      temperature: 0.1
    })

    const extractedText = response.choices[0]?.message?.content
    if (!extractedText) {
      return NextResponse.json(
        { error: 'No text extracted' },
        { status: 400 }
      )
    }

    return NextResponse.json({ text: extractedText.trim() })
  } catch (error: any) {
    console.error('Error extracting text with Kimi:', error)
    return NextResponse.json(
      { error: error.message || '文字识别失败,请稍后重试' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/ocr/route.ts
================================================
import { NextResponse } from 'next/server'
import * as tencentcloud from 'tencentcloud-sdk-nodejs-ocr'

const OcrClient = tencentcloud.ocr.v20181119.Client

interface TextDetection {
  DetectedText: string;
  Confidence: number;
  Polygon: Array<{
    X: number;
    Y: number;
  }>;
  AdvancedInfo: string;
}

interface OCRResponse {
  TextDetections: TextDetection[];
  Language: string;
  RequestId: string;
}

const client = new OcrClient({
  credential: {
    secretId: process.env.TENCENT_SECRET_ID || '',
    secretKey: process.env.TENCENT_SECRET_KEY || '',
  },
  region: 'ap-guangzhou',
  profile: {
    signMethod: 'TC3-HMAC-SHA256',
    httpProfile: {
      reqMethod: 'POST',
      reqTimeout: 30,
      endpoint: 'ocr.tencentcloudapi.com',
    },
  },
})

export async function POST(request: Request) {
  try {
    const { image } = await request.json()

    if (!image) {
      return NextResponse.json(
        { success: false, message: '未找到图片数据' },
        { status: 400 }
      )
    }

    const result = await client.GeneralBasicOCR({
      ImageBase64: image,
    }) as OCRResponse

    if (!result || !result.TextDetections || result.TextDetections.length === 0) {
      return NextResponse.json(
        { success: false, message: '未识别到文字' },
        { status: 400 }
      )
    }

    const text = result.TextDetections.map((item: TextDetection) => item.DetectedText).join('\n')

    return NextResponse.json({
      success: true,
      text
    })
  } catch (error: any) {
    console.error('腾讯云OCR错误:', error)
    return NextResponse.json(
      { success: false, message: error.message || 'OCR识别失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/ocr/step/route.ts
================================================
import { NextResponse } from 'next/server'
import OpenAI from 'openai'

// 设置较长的超时时间
export const maxDuration = 60; // 设置为60秒

export async function POST(request: Request) {
  const apiKey = process.env.NEXT_PUBLIC_STEP_API_KEY;
  if (!apiKey) {
    return NextResponse.json(
      { error: 'API key not configured' },
      { status: 500 }
    );
  }

  try {
    const formData = await request.formData();
    const image = formData.get('image');
    
    if (!image) {
      return NextResponse.json(
        { error: 'No image provided' },
        { status: 400 }
      );
    }

    const openai = new OpenAI({
      apiKey: apiKey,
      baseURL: 'https://api.stepfun.com/v1',
      dangerouslyAllowBrowser: true,
      timeout: 30000 // 30秒超时
    });

    // 最多重试3次
    let retries = 3;
    let lastError;

    while (retries > 0) {
      try {
        const response = await openai.chat.completions.create({
          model: 'step-1v-32k',
          messages: [
            {
              role: 'system',
              content: 'You are an OCR assistant. Extract text from the provided image accurately.'
            },
            {
              role: 'user',
              content: [
                { type: 'text', text: 'Please extract text from this image:' },
                { type: 'image_url', image_url: { url: image instanceof File ? URL.createObjectURL(image) : image.toString() } }
              ]
            }
          ]
        });

        const extractedText = response.choices[0]?.message?.content;
        if (!extractedText) {
          throw new Error('No text extracted');
        }

        return NextResponse.json({ text: extractedText });
      } catch (error: any) {
        lastError = error;
        if (error.status === 504) {
          retries--;
          if (retries > 0) {
            // 等待1秒后重试
            await new Promise(resolve => setTimeout(resolve, 1000));
            continue;
          }
        }
        break;
      }
    }

    console.error('Step OCR error after retries:', lastError);
    return NextResponse.json(
      { error: lastError?.message || 'OCR failed after retries' },
      { status: lastError?.status || 500 }
    );
  } catch (error: any) {
    console.error('Step OCR error:', error);
    return NextResponse.json(
      { error: error.message || 'OCR failed' },
      { status: error.status || 500 }
    );
  }
} 

================================================
FILE: app/api/qwen/ocr/route.ts
================================================
import { NextResponse } from 'next/server'

if (!process.env.NEXT_PUBLIC_QWEN_API_KEY) {
  throw new Error('Missing NEXT_PUBLIC_QWEN_API_KEY environment variable')
}

export async function POST(request: Request) {
  try {
    const { image } = await request.json()

    if (!image) {
      return NextResponse.json(
        { message: '缺少图片数据' },
        { status: 400 }
      )
    }

    const response = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_QWEN_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'qwen-vl-ocr',
        messages: [
          {
            role: 'user',
            content: [
              {
                type: 'image_url',
                image_url: {
                  url: `data:image/jpeg;base64,${image}`
                },
                min_pixels: 28 * 28 * 4,
                max_pixels: 28 * 28 * 1280
              },
              {
                type: 'text',
                text: 'Read all the text in the image.'
              }
            ]
          }
        ]
      })
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(error.message || '文字识别失败')
    }

    const result = await response.json()
    const text = result.choices[0].message.content
    return NextResponse.json({ text })
  } catch (error: any) {
    console.error('通义千问OCR错误:', error)
    return NextResponse.json(
      { message: error.message || '文字识别失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/qwen/translate/route.ts
================================================
import { NextResponse } from 'next/server';

export const runtime = 'edge';

export async function POST(request: Request) {
  try {
    const { text, targetLang } = await request.json();

    if (!text || !targetLang) {
      return NextResponse.json(
        { error: '缺少必要参数' },
        { status: 400 }
      );
    }

    const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_QWEN_API_KEY}`,
      },
      body: JSON.stringify({
        model: 'qwen-max',
        input: {
          messages: [
            {
              role: 'system',
              content: 'You are a professional translator. Translate the text directly without any explanations.'
            },
            {
              role: 'user',
              content: `Translate to ${targetLang}:\n${text}`
            }
          ]
        },
        parameters: {
          temperature: 0.1,
          max_tokens: 2048,
        }
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      return NextResponse.json(
        { error: error.message || '翻译请求失败' },
        { status: response.status }
      );
    }

    const result = await response.json();
    return NextResponse.json({ text: result.output.text });
  } catch (error: any) {
    console.error('Error in Qwen translation:', error);
    return NextResponse.json(
      { error: error.message || '翻译服务出错' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/register/route.ts
================================================
import { NextResponse } from 'next/server'
import { neon } from '@neondatabase/serverless'
import bcrypt from 'bcryptjs'

const sql = neon(process.env.DATABASE_URL!)

export async function POST(req: Request) {
  try {
    console.log('开始处理注册请求')
    const { email, password } = await req.json()
    console.log('收到注册数据:', { email, hasPassword: !!password })

    // 验证输入
    if (!email || !password) {
      console.log('输入验证失败')
      return NextResponse.json(
        { error: '请输入邮箱和密码' },
        { status: 400 }
      )
    }

    // 检查邮箱是否已存在
    console.log('检查邮箱是否存在:', email)
    const existingUser = await sql`
      SELECT id FROM auth_users WHERE email = ${email}
    `
    console.log('查询结果:', existingUser)
    
    if (existingUser.length > 0) {
      console.log('邮箱已存在')
      return NextResponse.json(
        { error: '该邮箱已被注册' },
        { status: 400 }
      )
    }

    // 加密密码
    console.log('开始加密密码')
    const hashedPassword = await bcrypt.hash(password, 10)

    // 创建用户
    console.log('开始创建用户')
    const result = await sql`
      INSERT INTO auth_users (
        email, 
        password_hash,
        text_quota,
        image_quota,
        pdf_quota,
        speech_quota,
        video_quota
      )
      VALUES (
        ${email}, 
        ${hashedPassword},
        -1,
        10,
        8,
        5,
        2
      )
      RETURNING *
    `
    console.log('创建的用户数据:', result[0])

    return NextResponse.json(
      { 
        message: '注册成功',
        user: {
          id: result[0].id,
          email: result[0].email
        }
      },
      { status: 201 }
    )
  } catch (error: any) {
    console.error('注册错误:', error)
    console.error('错误堆栈:', error.stack)
    return NextResponse.json(
      { error: error.message || '注册失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/subscription/route.ts
================================================
import { getServerSession } from 'next-auth'
import { NextResponse } from 'next/server'
import { stripe, PLANS } from '@/lib/stripe'
import { authOptions } from '../auth/[...nextauth]/auth'

export async function POST(req: Request) {
  try {
    const session = await getServerSession(authOptions)
    if (!session?.user?.email) {
      return new NextResponse('Unauthorized', { status: 401 })
    }

    const { priceId } = await req.json()
    if (!priceId) {
      return new NextResponse('Price ID is required', { status: 400 })
    }

    if (!stripe) {
      return new NextResponse('Stripe is not configured', { status: 500 })
    }

    // 检查价格 ID 是否有效
    const paidPlans = [PLANS.monthly, PLANS.yearly]
    const plan = paidPlans.find(p => p.priceId === priceId)
    if (!plan) {
      return new NextResponse('Invalid price ID', { status: 400 })
    }

    // 创建或获取 Stripe 客户
    const customer = await stripe.customers.create({
      email: session.user.email,
      metadata: {
        userId: session.user.id
      }
    })

    // 创建结账会话
    const checkoutSession = await stripe.checkout.sessions.create({
      customer: customer.id,
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/profile?subscription=success`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
      subscription_data: {
        metadata: {
          userId: session.user.id,
        },
      },
    })

    return NextResponse.json({ url: checkoutSession.url })
  } catch (error) {
    console.error('Subscription error:', error)
    return new NextResponse('Internal error', { status: 500 })
  }
} 

================================================
FILE: app/api/tencent/ocr/route.ts
================================================
import { NextResponse } from 'next/server';

// 使用 require 导入腾讯云 SDK
const tencentcloud = require("tencentcloud-sdk-nodejs");
const OcrClient = tencentcloud.ocr.v20181119.Client;

export async function POST(request: Request) {
  try {
    const { image } = await request.json();

    if (!image) {
      return NextResponse.json(
        { success: false, message: '缺少图片数据' },
        { status: 400 }
      );
    }

    const client = new OcrClient({
      credential: {
        secretId: process.env.TENCENT_SECRET_ID || '',
        secretKey: process.env.TENCENT_SECRET_KEY || '',
      },
      region: 'ap-guangzhou',
      profile: {
        signMethod: 'TC3-HMAC-SHA256',
        httpProfile: {
          reqMethod: 'POST',
          reqTimeout: 30,
          endpoint: 'ocr.tencentcloudapi.com',
        },
      },
    });

    const base64Data = image.split(',')[1];
    const result = await client.GeneralBasicOCR({
      ImageBase64: base64Data,
      LanguageType: 'auto',
    });

    if (!result || !result.TextDetections) {
      throw new Error('文字识别失败');
    }

    const textLines = result.TextDetections.map((item: any) => item.DetectedText).filter(Boolean);
    const text = textLines.join('\n');

    return NextResponse.json({
      success: true,
      result: text
    });
  } catch (error: any) {
    console.error('腾讯云 OCR 错误:', error);
    return NextResponse.json(
      { 
        success: false, 
        message: error.code === 'AuthFailure' 
          ? '腾讯云认证失败,请检查密钥配置' 
          : '文字识别失败'
      },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/translate/claude/route.ts
================================================
import { NextResponse } from 'next/server';
import OpenAI from 'openai';

export async function POST(request: Request) {
  try {
    const { text, targetLanguage } = await request.json();

    if (!text || !targetLanguage) {
      return NextResponse.json(
        { error: '缺少必要参数' },
        { status: 400 }
      );
    }

    const apiKey = process.env.NEXT_PUBLIC_OPENROUTER_API_KEY;
    if (!apiKey) {
      return NextResponse.json(
        { error: 'OpenRouter API key not found' },
        { status: 500 }
      );
    }

    const openai = new OpenAI({
      apiKey: apiKey,
      baseURL: 'https://openrouter.ai/api/v1'
    });

    const completion = await openai.chat.completions.create({
      model: 'anthropic/claude-3-haiku',
      messages: [
        {
          role: 'system',
          content: 'You are a professional translator. Translate the text directly without any explanations.'
        },
        {
          role: 'user',
          content: `Translate to ${targetLanguage}:\n${text}`
        }
      ],
      temperature: 0.3,
      max_tokens: 2000,
    });

    const translatedText = completion.choices[0].message.content;
    return NextResponse.json({ text: translatedText });

  } catch (error: any) {
    console.error('Claude translation error:', error);
    return NextResponse.json(
      { error: error.message || '翻译失败' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/translate/kimi/route.ts
================================================
import { NextResponse } from 'next/server';
import OpenAI from 'openai';

export async function POST(request: Request) {
  try {
    const { text, targetLanguage } = await request.json();

    if (!text || !targetLanguage) {
      return NextResponse.json(
        { error: 'Missing required parameters' },
        { status: 400 }
      );
    }

    const apiKey = process.env.NEXT_PUBLIC_KIMI_API_KEY;
    if (!apiKey) {
      return NextResponse.json(
        { error: 'API key not found' },
        { status: 500 }
      );
    }

    const openai = new OpenAI({
      apiKey: apiKey,
      baseURL: 'https://api.moonshot.cn/v1',
    });

    const completion = await openai.chat.completions.create({
      model: 'moonshot-v1-128k',
      messages: [
        {
          role: 'system',
          content: `You are a professional translator. Translate the following text to ${targetLanguage}. Keep the original format and style.`
        },
        {
          role: 'user',
          content: text
        }
      ],
      temperature: 0.3,
      max_tokens: 2000
    });

    const translatedText = completion.choices[0]?.message?.content;
    if (!translatedText) {
      return NextResponse.json(
        { error: 'No translation result' },
        { status: 500 }
      );
    }

    return NextResponse.json({ translatedText });
  } catch (error: any) {
    console.error('Kimi translation error:', error);
    return NextResponse.json(
      { error: error.message || 'Translation failed' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/translate/route.ts
================================================
import OpenAI from 'openai';
import { sign } from '@/lib/server/tencent-sign';
import { translateWithKimiAPI } from '@/lib/server/translate';

export async function POST(request: Request) {
  try {
    const { text, targetLanguage, service } = await request.json();

    if (!text || !targetLanguage) {
      return new Response(JSON.stringify({ error: '缺少必要参数' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    let translatedText;

    try {
      switch (service) {
        case 'deepseek':
          translatedText = await translateWithDeepSeekAPI(text, targetLanguage);
          break;
        case 'qwen':
          translatedText = await translateWithQwenAPI(text, targetLanguage);
          break;
        case 'zhipu':
          translatedText = await translateWithZhipuAPI(text, targetLanguage);
          break;
        case '4o-mini':
          translatedText = await translateWith4oMiniAPI(text, targetLanguage);
          break;
        case 'hunyuan':
          translatedText = await translateWithHunyuanAPI(text, targetLanguage);
          break;
        case 'minnimax':
          translatedText = await translateWithMinniMaxAPI(text, targetLanguage);
          break;
        case 'siliconflow':
          translatedText = await translateWithSiliconFlowAPI(text, targetLanguage);
          break;
        case 'claude_3_5':
          translatedText = await translateWithClaudeAPI(text, targetLanguage);
          break;
        case 'kimi':
          translatedText = await translateWithKimiAPI(text, targetLanguage);
          break;
        case 'step':
          translatedText = await translateWithStepAPI(text, targetLanguage);
          break;
        default:
          translatedText = await translateWithDeepSeekAPI(text, targetLanguage);
      }
    } catch (serviceError: any) {
      console.error(`${service} translation service error:`, serviceError);
      // 如果当前服务失败,尝试使用 DeepSeek 作为备选
      if (service !== 'deepseek') {
        console.log('Trying DeepSeek as fallback service...');
        translatedText = await translateWithDeepSeekAPI(text, targetLanguage);
      } else {
        throw serviceError;
      }
    }

    return new Response(JSON.stringify({ text: translatedText }), {
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error: any) {
    console.error('Translation error:', error);
    return new Response(JSON.stringify({ error: error.message || '翻译失败' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

async function translateWithDeepSeekAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY;
  if (!apiKey) {
    throw new Error('DeepSeek API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://api.deepseek.com/v1'
  });

  const response = await openai.chat.completions.create({
    model: 'deepseek-chat',
    messages: [
      {
        role: 'system',
        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`
      },
      {
        role: 'user',
        content: text
      }
    ],
    temperature: 0.3,
    max_tokens: 2000
  });

  return response.choices[0].message.content || '';
}

async function translateWithQwenAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_QWEN_API_KEY;
  if (!apiKey) {
    throw new Error('Qwen API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
  });

  const response = await openai.chat.completions.create({
    model: 'qwen-max',
    messages: [
      {
        role: 'system',
        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`
      },
      {
        role: 'user',
        content: text
      }
    ],
    temperature: 0.3,
    max_tokens: 2000
  });

  return response.choices[0].message.content || '';
}

async function translateWithZhipuAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_ZHIPU_API_KEY;
  if (!apiKey) {
    throw new Error('Zhipu API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://open.bigmodel.cn/api/paas/v4'
  });

  const response = await openai.chat.completions.create({
    model: 'glm-4',
    messages: [
      {
        role: 'system',
        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`
      },
      {
        role: 'user',
        content: text
      }
    ],
    temperature: 0.3,
    max_tokens: 2000
  });

  return response.choices[0].message.content || '';
}

async function translateWith4oMiniAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
  if (!apiKey) {
    throw new Error('OpenAI API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://api.openai.com/v1'
  });

  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`
      },
      {
        role: 'user',
        content: text
      }
    ],
    temperature: 0.3,
    max_tokens: 2000
  });

  return response.choices[0].message.content || '';
}

async function translateWithHunyuanAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_TENCENT_API_KEY;
  if (!apiKey) {
    throw new Error('Tencent API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://api.hunyuan.cloud.tencent.com/v1'
  });

  const response = await openai.chat.completions.create({
    model: 'hunyuan-turbo',
    messages: [
      {
        role: 'system',
        content: `你是一个专业的翻译助手,请直接翻译文本,不要添加任何解释。`
      },
      {
        role: 'user',
        content: `将以下文本翻译成${targetLanguage}:\n\n${text}`
      }
    ],
    temperature: 0.1,
    top_p: 0.7,
    // @ts-expect-error key is not yet public
    enable_enhancement: true
  });

  return response.choices[0].message.content || '';
}

async function translateWithMinniMaxAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_MINNIMAX_API_KEY;
  if (!apiKey) {
    throw new Error('MinniMax API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://api.minimax.chat/v1',
  });

  try {
    const completion = await openai.chat.completions.create({
      model: 'abab6.5s-chat',
      messages: [
        {
          role: 'system',
          content: 'You are a professional translator. Translate the text directly without any explanations.'
        },
        {
          role: 'user',
          content: `Translate to ${targetLanguage}:\n${text}`
        }
      ],
      temperature: 0.3,
      max_tokens: 2000,
    });

    return completion.choices[0].message.content;
  } catch (error: any) {
    console.error('MinniMax translation error:', error);
    throw new Error(error.message || '翻译失败');
  }
}

async function translateWithSiliconFlowAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_SILICONFLOW_API_KEY;
  if (!apiKey) {
    throw new Error('SiliconFlow API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://api.siliconflow.com/v1'
  });

  try {
    const completion = await openai.chat.completions.create({
      model: 'meta-llama/Llama-3.3-70B-Instruct',
      messages: [
        {
          role: 'system',
          content: 'You are a professional translator. Translate the text directly without any explanations.'
        },
        {
          role: 'user',
          content: `Translate to ${targetLanguage}:\n${text}`
        }
      ],
      temperature: 0.3,
      max_tokens: 2000,
    });

    return completion.choices[0].message.content;
  } catch (error: any) {
    console.error('SiliconFlow translation error:', error);
    throw new Error(error.message || '翻译失败');
  }
}

async function translateWithClaudeAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_OPENROUTER_API_KEY;
  if (!apiKey) {
    throw new Error('OpenRouter API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://openrouter.ai/api/v1'
  });

  const completion = await openai.chat.completions.create({
    model: 'anthropic/claude-3-haiku',
    messages: [
      {
        role: 'system',
        content: 'You are a professional translator. Translate the text directly without any explanations.'
      },
      {
        role: 'user',
        content: `Translate to ${targetLanguage}:\n${text}`
      }
    ],
    temperature: 0.3,
    max_tokens: 2000,
  });

  return completion.choices[0].message.content || '';
}

async function translateWithStepAPI(text: string, targetLanguage: string) {
  const apiKey = process.env.NEXT_PUBLIC_STEP_API_KEY;
  if (!apiKey) {
    throw new Error('Step API key not found');
  }

  const openai = new OpenAI({
    apiKey: apiKey,
    baseURL: 'https://api.stepfun.com/v1'
  });

  const response = await openai.chat.completions.create({
    model: 'step-2-16k',
    messages: [
      {
        role: 'system',
        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`
      },
      {
        role: 'user',
        content: text
      }
    ],
    temperature: 0.3,
    max_tokens: 2000
  });

  return response.choices[0].message.content || '';
} 

================================================
FILE: app/api/translate/siliconflow/route.ts
================================================
import { NextResponse } from 'next/server';
import OpenAI from 'openai';

export async function POST(request: Request) {
  try {
    const { text, targetLanguage } = await request.json();

    if (!text || !targetLanguage) {
      return NextResponse.json(
        { error: '缺少必要参数' },
        { status: 400 }
      );
    }

    const apiKey = process.env.NEXT_PUBLIC_SILICONFLOW_API_KEY;
    if (!apiKey) {
      return NextResponse.json(
        { error: 'SiliconFlow API key not found' },
        { status: 500 }
      );
    }

    const openai = new OpenAI({
      apiKey: apiKey,
      baseURL: 'https://api.siliconflow.com/v1'
    });

    const completion = await openai.chat.completions.create({
      model: 'meta-llama/Llama-3.3-70B-Instruct',
      messages: [
        {
          role: 'system',
          content: 'You are a professional translator. Translate the text directly without any explanations.'
        },
        {
          role: 'user',
          content: `Translate to ${targetLanguage}:\n${text}`
        }
      ],
      temperature: 0.3,
      max_tokens: 2000,
    });

    const translatedText = completion.choices[0].message.content;
    return NextResponse.json({ text: translatedText });

  } catch (error: any) {
    console.error('SiliconFlow translation error:', error);
    return NextResponse.json(
      { error: error.message || '翻译失败' },
      { status: 500 }
    );
  }
} 

================================================
FILE: app/api/translate/step/route.ts
================================================
import { NextResponse } from 'next/server'
import OpenAI from 'openai'

export async function POST(req: Request) {
  try {
    const apiKey = process.env.NEXT_PUBLIC_STEP_API_KEY;
    if (!apiKey) {
      return new NextResponse('API key not configured', { status: 500 })
    }

    const openai = new OpenAI({
      apiKey: apiKey,
      baseURL: 'https://api.stepfun.com/v1'
    })

    const { text, targetLanguage } = await req.json()

    if (!text) {
      return new NextResponse('Text is required', { status: 400 })
    }

    if (!targetLanguage) {
      return new NextResponse('Target language is required', { status: 400 })
    }

    const completion = await openai.chat.completions.create({
      model: 'step-2-16k',
      messages: [
        {
          role: 'system',
          content: `You are a professional translator. Translate the following text to ${targetLanguage}. Keep the original format and style.`
        },
        {
          role: 'user',
          content: text
        }
      ],
      temperature: 0.3,
      max_tokens: 2000
    })

    const translation = completion.choices[0]?.message?.content || ''
    if (!translation) {
      return new NextResponse('No translation result', { status: 500 })
    }

    return NextResponse.json({ translation })
  } catch (error: any) {
    console.error('Translation error:', error)
    return new NextResponse(error.message || 'Internal Server Error', {
      status: error.status || 500,
    })
  }
} 

================================================
FILE: app/api/upload/route.ts
================================================
import { NextResponse } from 'next/server'
import { put } from '@vercel/blob'

export async function POST(request: Request) {
  try {
    const { file, type } = await request.json()

    if (!file) {
      return NextResponse.json(
        { error: '未提供文件' },
        { status: 400 }
      )
    }

    // 从 base64 中提取实际的文件数据
    const base64Data = file.replace(/^data:.*?;base64,/, '')
    const buffer = Buffer.from(base64Data, 'base64')

    // 生成唯一的文件名
    const filename = `${Date.now()}-${Math.random().toString(36).substring(7)}.${type === 'video' ? 'mp4' : 'jpg'}`

    // 上传到 Vercel Blob
    const blob = await put(filename, buffer, {
      access: 'public',
      contentType: type === 'video' ? 'video/mp4' : 'image/jpeg'
    })

    return NextResponse.json({ url: blob.url })
  } catch (error: any) {
    console.error('文件上传错误:', error)
    return NextResponse.json(
      { error: error.message || '文件上传失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/user/info/route.ts
================================================
import { getServerSession } from 'next-auth'
import { NextResponse } from 'next/server'
import { neon } from '@neondatabase/serverless'
import { authOptions } from '../../auth/[...nextauth]/auth'

interface User {
  id: string;
  email: string;
  name: string | null;
  github_id: string | null;
  google_id: string | null;
  stripe_customer_id: string | null;
  stripe_subscription_id: string | null;
  stripe_price_id: string | null;
  stripe_current_period_end: string | null;
  text_quota: number;
  image_quota: number;
  pdf_quota: number;
  speech_quota: number;
  video_quota: number;
  quota_reset_at: string | null;
  created_at: string;
  updated_at: string;
}

type UsageType = 'text' | 'image' | 'pdf' | 'speech' | 'video';

interface UsageRecord {
  type: UsageType;
  count: string;
}

interface UsageInfo {
  [key: string]: number;
  text: number;
  image: number;
  pdf: number;
  speech: number;
  video: number;
}

export async function GET() {
  try {
    console.log('开始获取用户信息')
    const session = await getServerSession(authOptions)
    if (!session?.user?.email) {
      console.log('未找到用户会话')
      return new NextResponse('Unauthorized', { status: 401 })
    }

    console.log('用户邮箱:', session.user.email)
    const sql = neon(process.env.DATABASE_URL!)
    
    // 获取用户基本信息和订阅信息
    const users = await sql`
      SELECT 
        id,
        email,
        name,
        github_id,
        google_id,
        stripe_customer_id,
        stripe_subscription_id,
        stripe_price_id,
        stripe_current_period_end,
        text_quota,
        image_quota,
        pdf_quota,
        speech_quota,
        video_quota,
        quota_reset_at,
        created_at,
        updated_at
      FROM auth_users 
      WHERE email = ${session.user.email}
    ` as User[]

    if (!users.length) {
      console.log('未找到用户记录')
      return new NextResponse('User not found', { status: 404 })
    }

    const user = users[0]
    console.log('查询到的用户信息:', {
      id: user.id,
      email: user.email,
      subscription: {
        customerId: user.stripe_customer_id,
        subscriptionId: user.stripe_subscription_id,
        priceId: user.stripe_price_id,
        currentPeriodEnd: user.stripe_current_period_end
      },
      quotas: {
        text: user.text_quota,
        image: user.image_quota,
        pdf: user.pdf_quota,
        speech: user.speech_quota,
        video: user.video_quota
      }
    })

    // 检查订阅是否已过期
    if (user.stripe_current_period_end) {
      const currentPeriodEnd = new Date(user.stripe_current_period_end).getTime()
      const now = new Date().getTime()
      
      if (now > currentPeriodEnd) {
        console.log('订阅已过期,重置为试用版')
        await sql`
          UPDATE auth_users 
          SET 
            stripe_subscription_id = NULL,
            stripe_price_id = NULL,
            stripe_current_period_end = NULL,
            text_quota = -1,
            image_quota = 5,
            pdf_quota = 3,
            speech_quota = 2,
            video_quota = 1
          WHERE id = ${user.id}
        `
        
        user.stripe_subscription_id = null
        user.stripe_price_id = null
        user.stripe_current_period_end = null
        user.text_quota = -1
        user.image_quota = 5
        user.pdf_quota = 3
        user.speech_quota = 2
        user.video_quota = 1
      }
    }

    const today = new Date().toISOString().split('T')[0]
    console.log('当前日期:', today, '上次配额重置日期:', user.quota_reset_at)

    // 如果配额重置日期不是今天,根据用户的订阅计划重置配额
    if (user.quota_reset_at !== today) {
      console.log('重置用户配额,订阅计划:', user.stripe_price_id)
      
      let quotaUpdate
      if (user.stripe_price_id === process.env.NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID) {
        quotaUpdate = {
          text_quota: -1,
          image_quota: 50,
          pdf_quota: 40,
          speech_quota: 30,
          video_quota: 10
        }
      } else if (user.stripe_price_id === process.env.NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID) {
        quotaUpdate = {
          text_quota: -1,
          image_quota: 100,
          pdf_quota: 80,
          speech_quota: 60,
          video_quota: 20
        }
      } else {
        quotaUpdate = {
          text_quota: -1,
          image_quota: 5,
          pdf_quota: 3,
          speech_quota: 2,
          video_quota: 1
        }
      }

      await sql`
        UPDATE auth_users
        SET 
          image_quota = ${quotaUpdate.image_quota},
          pdf_quota = ${quotaUpdate.pdf_quota},
          speech_quota = ${quotaUpdate.speech_quota},
          video_quota = ${quotaUpdate.video_quota},
          quota_reset_at = ${today}
        WHERE id = ${user.id}
      `
      
      user.text_quota = quotaUpdate.text_quota
      user.image_quota = quotaUpdate.image_quota
      user.pdf_quota = quotaUpdate.pdf_quota
      user.speech_quota = quotaUpdate.speech_quota
      user.video_quota = quotaUpdate.video_quota
      console.log('配额重置完成,新配额:', quotaUpdate)
    }

    // 获取今日使用次数
    console.log('开始获取今日使用次数')
    const usage = await sql`
      SELECT type, COUNT(*) as count
      FROM usage_records
      WHERE user_id = ${user.id}
        AND DATE(used_at) = CURRENT_DATE
      GROUP BY type
    ` as UsageRecord[]

    console.log('今日使用记录:', usage)

    // 构建响应数据
    const response = {
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        github_id: user.github_id,
        google_id: user.google_id,
        created_at: user.created_at,
        updated_at: user.updated_at
      },
      subscription: {
        stripe_customer_id: user.stripe_customer_id,
        stripe_subscription_id: user.stripe_subscription_id,
        stripe_price_id: user.stripe_price_id,
        stripe_current_period_end: user.stripe_current_period_end
      },
      quota: {
        text_quota: user.text_quota,
        image_quota: user.image_quota,
        pdf_quota: user.pdf_quota,
        speech_quota: user.speech_quota,
        video_quota: user.video_quota
      },
      usage: {
        text: 0,
        image: 0,
        pdf: 0,
        speech: 0,
        video: 0
      } as UsageInfo
    }

    // 填充使用次数
    usage.forEach((record) => {
      response.usage[record.type] = parseInt(record.count)
    })

    console.log('返回的用户信息:', response)
    return NextResponse.json(response)
  } catch (error) {
    console.error('获取用户信息失败:', error)
    return new NextResponse('Internal error', { status: 500 })
  }
} 

================================================
FILE: app/api/user/update/route.ts
================================================
import { getServerSession } from 'next-auth'
import { NextResponse } from 'next/server'
import { neon } from '@neondatabase/serverless'
import { authOptions } from '../../auth/[...nextauth]/auth'

export async function PUT(req: Request) {
  try {
    const session = await getServerSession(authOptions)
    if (!session?.user?.email) {
      return new NextResponse('Unauthorized', { status: 401 })
    }

    const body = await req.json()
    const { name } = body

    if (typeof name !== 'string' || name.length > 50) {
      return new NextResponse('Invalid name', { status: 400 })
    }

    const sql = neon(process.env.DATABASE_URL!)
    
    // 更新用户名
    await sql`
      UPDATE auth_users
      SET name = ${name}, updated_at = CURRENT_TIMESTAMP
      WHERE email = ${session.user.email}
    `

    return new NextResponse('OK')
  } catch (error) {
    console.error('Failed to update user:', error)
    return new NextResponse('Internal error', { status: 500 })
  }
} 

================================================
FILE: app/api/user/usage/route.ts
================================================
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/auth'
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)

// 检查配额是否足够
async function checkQuota(userId: number, type: string) {
  const quotaField = `${type}_quota`
  
  // 获取用户配额信息
  const result = await sql`
    SELECT ${sql(quotaField)}, quota_reset_at
    FROM auth_users
    WHERE id = ${userId}
  `
  
  if (result.length === 0) {
    throw new Error('用户不存在')
  }

  const user = result[0]
  const today = new Date().toISOString().split('T')[0]

  // 如果是新的一天,重置配额
  if (user.quota_reset_at !== today) {
    const defaultQuotas = {
      text: -1,
      image: 10,
      pdf: 8,
      speech: 5,
      video: 2
    }

    await sql`
      UPDATE auth_users
      SET 
        image_quota = ${defaultQuotas.image},
        pdf_quota = ${defaultQuotas.pdf},
        speech_quota = ${defaultQuotas.speech},
        video_quota = ${defaultQuotas.video},
        quota_reset_at = ${today}
      WHERE id = ${userId}
    `
    
    return defaultQuotas[type as keyof typeof defaultQuotas]
  }

  return user[quotaField]
}

// 获取今日使用次数
async function getUsageCount(userId: number, type: string) {
  const result = await sql`
    SELECT COUNT(*) as count
    FROM usage_records
    WHERE user_id = ${userId}
      AND type = ${type}
      AND DATE(used_at) = CURRENT_DATE
  `
  return parseInt(result[0].count)
}

// 记录使用并减少配额
async function recordUsage(userId: number, type: string) {
  console.log('开始记录使用情况:', { userId, type })
  
  try {
    // 开始事务
    await sql`BEGIN`

    try {
      // 插入使用记录
      await sql`
        INSERT INTO usage_records (user_id, type)
        VALUES (${userId}, ${type})
      `
      console.log('使用记录已插入')

      // 如果不是无限制配额,减少剩余次数
      if (type !== 'text') {
        // 根据类型选择不同的更新语句
        let updateResult;
        switch (type) {
          case 'image':
            updateResult = await sql`
              UPDATE auth_users 
              SET image_quota = GREATEST(image_quota - 1, 0),
                  updated_at = CURRENT_TIMESTAMP
              WHERE id = ${userId}
              RETURNING image_quota as remaining_quota
            `
            break;
          case 'pdf':
            updateResult = await sql`
              UPDATE auth_users 
              SET pdf_quota = GREATEST(pdf_quota - 1, 0),
                  updated_at = CURRENT_TIMESTAMP
              WHERE id = ${userId}
              RETURNING pdf_quota as remaining_quota
            `
            break;
          case 'speech':
            updateResult = await sql`
              UPDATE auth_users 
              SET speech_quota = GREATEST(speech_quota - 1, 0),
                  updated_at = CURRENT_TIMESTAMP
              WHERE id = ${userId}
              RETURNING speech_quota as remaining_quota
            `
            break;
          case 'video':
            updateResult = await sql`
              UPDATE auth_users 
              SET video_quota = GREATEST(video_quota - 1, 0),
                  updated_at = CURRENT_TIMESTAMP
              WHERE id = ${userId}
              RETURNING video_quota as remaining_quota
            `
            break;
        }
        console.log('配额已更新,剩余:', updateResult?.[0]?.remaining_quota)
      }

      // 提交事务
      await sql`COMMIT`
      console.log('事务已提交')
    } catch (error) {
      // 如果出错,回滚事务
      await sql`ROLLBACK`
      console.error('事务回滚:', error)
      throw error
    }
  } catch (error) {
    console.error('记录使用情况失败:', error)
    throw error
  }
}

export async function POST(req: Request) {
  try {
    const session = await getServerSession(authOptions)
    console.log('当前会话:', session?.user)
    
    if (!session?.user?.email) {
      return NextResponse.json(
        { error: '未登录' },
        { status: 401 }
      )
    }

    const { type } = await req.json()
    
    if (!type || !['text', 'image', 'pdf', 'speech', 'video'].includes(type)) {
      return NextResponse.json(
        { error: '无效的使用类型' },
        { status: 400 }
      )
    }

    // 获取用户ID
    const users = await sql`
      SELECT id FROM auth_users WHERE email = ${session.user.email}
    `

    if (users.length === 0) {
      return NextResponse.json(
        { error: '用户不存在' },
        { status: 404 }
      )
    }

    const userId = users[0].id

    // 检查配额
    const quota = await checkQuota(userId, type)
    const usageCount = await getUsageCount(userId, type)

    // 如果是无限制配额,直接记录使用
    if (quota === -1) {
      await recordUsage(userId, type)
      return NextResponse.json({
        success: true,
        remaining: -1
      })
    }

    // 计算剩余次数
    const remainingQuota = quota - usageCount

    console.log('配额检查:', {
      type,
      quota,
      usageCount,
      remainingQuota
    })

    // 如果没有剩余次数,返回错误
    if (remainingQuota <= 0) {
      return NextResponse.json(
        { error: '今日使用次数已达上限' },
        { status: 403 }
      )
    }

    // 记录使用
    await recordUsage(userId, type)

    // 返回更新后的配额信息
    console.log('更新后的配额:', {
      type,
      quota,
      usageCount: usageCount + 1,
      remaining: remainingQuota - 1
    })

    return NextResponse.json({
      success: true,
      remaining: remainingQuota - 1
    })

  } catch (error: any) {
    console.error('记录使用情况失败:', error)
    return NextResponse.json(
      { error: error.message || '记录使用失败' },
      { status: 500 }
    )
  }
} 

================================================
FILE: app/api/webhook/route.ts
================================================
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { stripe } from '@/lib/stripe'
import { neon } from '@neondatabase/serverless'

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!

export async function POST(req: Request) {
  console.log('收到 Stripe Webhook 请求')
  const body = await req.text()
  const signature = headers().get('stripe-signature')

  let event

  try {
    if (!stripe) {
      console.error('Stripe 未配置')
      return new NextResponse('Stripe is not configured', { status: 500 })
    }
    
    event = stripe.webhooks.constructEvent(
      body,
      signature!,
      webhookSecret
    )
    console.log('Webhook 事件验证成功:', event.type)
  } catch (err: any) {
    console.error('Webhook 签名验证失败:', err)
    return new NextResponse(`Webhook Error: ${err.message}`, { status: 400 })
  }

  const sql = neon(process.env.DATABASE_URL!)

  try {
    switch (event.type) {
      case 'customer.subscription.created':
      case 'customer.subscription.updated': {
        const subscription = event.data.object
        const userId = subscription.metadata.userId
        const priceId = subscription.items.data[0].price.id

        console.log('处理订阅事件:', {
          type: event.type,
          userId,
          priceId,
          customerId: subscription.customer,
          subscriptionId: subscription.id,
          currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString()
        })

        // 根据价格ID设置对应的配额
        const quotaUpdate = priceId === process.env.NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID ? {
          text_quota: -1,
          image_quota: 50,
          pdf_quota: 40,
          speech_quota: 30,
          video_quota: 10
        } : priceId === process.env.NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID ? {
          text_quota: -1,
          image_quota: 100,
          pdf_quota: 80,
          speech_quota: 60,
          video_quota: 20
        } : {
          text_quota: -1,
          image_quota: 10,
          pdf_quota: 8,
          speech_quota: 5,
          video_quota: 2
        }

        console.log('执行数据库更新:', {
          customerId: subscription.customer,
          subscriptionId: subscription.id,
          priceId: subscription.items.data[0].price.id,
          currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(),
          quotaUpdate
        })

        // 更新用户订阅状态和配额
        const updateResult = await sql`
          UPDATE auth_users 
          SET 
            stripe_customer_id = ${subscription.customer},
            stripe_subscription_id = ${subscription.id},
            stripe_price_id = ${subscription.items.data[0].price.id},
            stripe_current_period_end = to_timestamp(${subscription.current_period_end}),
            text_quota = ${quotaUpdate.text_quota},
            image_quota = ${quotaUpdate.image_quota},
            pdf_quota = ${quotaUpdate.pdf_quota},
            speech_quota = ${quotaUpdate.speech_quota},
            video_quota = ${quotaUpdate.video_quota}
          WHERE id = ${userId}
          RETURNING *
        `
        console.log('数据库更新结果:', updateResult[0])
        break
      }

      case 'customer.subscription.deleted': {
        const subscription = event.data.object
        const userId = subscription.metadata.userId

        // 重置用户为免费计划
        await sql`
          UPDATE auth_users 
          SET 
            stripe_subscription_id = NULL,
            stripe_price_id = NULL,
            stripe_current_period_end = NULL,
            text_quota = -1,
            image_quota = 5,
            pdf_quota = 3,
            speech_quota = 2,
            video_quota = 1
          WHERE id = ${userId}
        `
        break
      }

      case 'invoice.payment_succeeded': {
        const invoice = event.data.object
        const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
        const userId = subscription.metadata.userId

        // 更新发票支付状态
        await sql`
          INSERT INTO payment_history (
            user_id,
            stripe_invoice_id,
            amount,
            status,
            payment_date
          ) VALUES (
            ${userId},
            ${invoice.id},
            ${invoice.amount_paid},
            'succeeded',
            to_timestamp(${invoice.created})
          )
        `
        break
      }

      case 'invoice.payment_failed': {
        const invoice = event.data.object
        const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
        const userId = subscription.metadata.userId

        // 记录支付失败
        await sql`
          INSERT INTO payment_history (
            user_id,
            stripe_invoice_id,
            amount,
            status,
            payment_date
          ) VALUES (
            ${userId},
            ${invoice.id},
            ${invoice.amount_due},
            'failed',
            to_timestamp(${invoice.created})
          )
        `

        // 可以在这里添加通知用户的逻辑
        break
      }
    }

    return new NextResponse(null, { status: 200 })
  } catch (error: any) {
    console.error('Webhook handler failed:', error)
    return new NextResponse('Webhook handler failed', { status: 500 })
  }
} 

================================================
FILE: app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    
    /* 更新为 #8cc63f 绿色 */
    --primary: 84 68% 51%;
    --primary-foreground: 210 40% 98%;
    
    /* 更新为匹配绿色的渐变色 */
    --secondary: 100 65% 45%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    
    /* 更新为匹配绿色的渐变色 */
    --accent: 70 70% 50%;
    --accent-foreground: 222.2 47.4% 11.2%;
    
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;
    --chart-1: 84 68% 51%;
    --chart-2: 70 70% 50%;
    --chart-3: 100 65% 45%;
    --chart-4: 120 60% 45%;
    --chart-5: 140 65% 40%;
    --radius: 0.5rem;
  }
  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    
    /* 更新为 #8cc63f 绿色 */
    --primary: 84 68% 51%;
    --primary-foreground: 222.2 47.4% 11.2%;
    
    /* 更新为匹配绿色的渐变色 */
    --secondary: 100 65% 45%;
    --secondary-foreground: 210 40% 98%;
    
    /* 更新为匹配绿色的渐变色 */
    --accent: 70 70% 50%;
    --accent-foreground: 210 40% 98%;
    
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
    --chart-1: 84 68% 51%;
    --chart-2: 70 70% 50%;
    --chart-3: 100 65% 45%;
    --chart-4: 120 60% 45%;
    --chart-5: 140 65% 40%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.animate-fadeIn {
  animation: fadeIn 0.5s ease-in forwards;
}

/* Swiper 自定义样式 */
.testimonials-swiper {
  padding-bottom: 3rem !important;
}

.testimonials-swiper .swiper-pagination-bullet {
  width: 10px;
  height: 10px;
  background: hsl(var(--muted-foreground));
  opacity: 0.5;
}

.testimonials-swiper .swiper-pagination-bullet-active {
  background: hsl(var(--primary));
  opacity: 1;
}

.testimonials-swiper .swiper-button-next,
.testimonials-swiper .swiper-button-prev {
  color: hsl(var(--primary));
  transition: all 0.2s;
}

.testimonials-swiper .swiper-button-next:hover,
.testimonials-swiper .swiper-button-prev:hover {
  transform: scale(1.1);
}

.testimonials-swiper .swiper-button-next::after,
.testimonials-swiper .swiper-button-prev::after {
  font-size: 1.5rem;
  font-weight: bold;
}

/* 暗色模式样式 */
.dark .testimonials-swiper .swiper-pagination-bullet {
  background: hsl(var(--muted-foreground));
}

.dark .testimonials-swiper .swiper-pagination-bullet-active {
  background: hsl(var(--primary));
}

.dark .testimonials-swiper .swiper-button-next,
.dark .testimonials-swiper .swiper-button-prev {
  color: hsl(var(--primary));
}


================================================
FILE: app/layout.tsx
================================================
import './globals.css';
import { Inter } from 'next/font/google';
import { Providers } from "./providers";
import GoogleAnalytics from '@/components/google-analytics';
import { LanguageProvider } from "@/components/language-provider";
import type { Metadata, Viewport } from 'next';

const inter = Inter({ subsets: ['latin'] });

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
};

export const metadata: Metadata = {
  metadataBase: new URL('https://aitranslate.site'),
  title: {
    template: '%s | AI Translation Assistant',
    default: 'AI Translation Assistant - Smart Multilingual Translation Platform',
  },
  description: 'All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation, making cross-language communication simpler.',
  keywords: ['AI Translation', 'Multilingual Translation', 'Image Translation', 'PDF Translation', 'Speech Translation', 'Video Translation', 'Machine Translation'],
  authors: [{ name: 'AI Translation Assistant Team' }],
  creator: 'AI Translation Assistant Team',
  publisher: 'AI Translation Assistant',
  icons: {
    icon: [
      { url: '/favicon.ico', sizes: 'any' },
      { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
      { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
    ],
    apple: [
      { url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
    ],
  },
  manifest: '/site.webmanifest',
  robots: {
    index: true,
    follow: true,
  },
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: '/',
    title: 'AI Translation Assistant - Smart Multilingual Translation Platform',
    description: 'All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation, making cross-language communication simpler.',
    siteName: 'AI Translation Assistant',
    images: [
      {
        url: '/og-image.png',
        width: 1200,
        height: 630,
        alt: 'AI Translation Assistant',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'AI Translation Assistant - Smart Multilingual Translation Platform',
    description: 'All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation, making cross-language communication simpler.',
    images: ['/og-image.png'],
  },
  verification: {
    google: 'yLQ9THm_U56rW0n0VsGzM6IXvWmlbS3fV7NGl-SZT3k',
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh" suppressHydrationWarning>
      <head>
        <link rel="manifest" href="/site.webmanifest" />
        <link rel="icon" href="/favicon.ico" sizes="any" />
        <link rel="icon" href="/favicon-16x16.png" type="image/png" sizes="16x16" />
        <link rel="icon" href="/favicon-32x32.png" type="image/png" sizes="32x32" />
        <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
      </head>
      <body className={inter.className}>
        <LanguageProvider>
          <Providers>
            <GoogleAnalytics />
            {children}
          </Providers>
        </LanguageProvider>
      </body>
    </html>
  );
}

================================================
FILE: app/login/error.tsx
================================================
'use client';

import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { useI18n } from '@/lib/i18n/use-translations';

export default function ErrorPage() {
  const { t } = useI18n();
  const searchParams = useSearchParams();
  const error = searchParams.get('error');

  useEffect(() => {
    if (error === 'AccessDenied') {
      toast.error(t('auth.error.accessDenied'));
    } else if (error) {
      toast.error(t('auth.error.default'));
    }
  }, [error, t]);

  return null;
} 

================================================
FILE: app/login/layout.tsx
================================================
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Sign in',
  description: 'Sign in to your account',
}

export default function LoginLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return children
} 

================================================
FILE: app/login/page.tsx
================================================
"use client";

import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'
import { signIn, useSession } from 'next-auth/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { toast } from 'sonner'
import { useI18n } from '@/lib/i18n/use-translations'
import { Github } from 'lucide-react'
import { Separator } from '@/components/ui/separator'

export default function LoginPage() {
  const { t } = useI18n()
  const router = useRouter()
  const searchParams = useSearchParams()
  const { data: session } = useSession()
  const [loading, setLoading] = useState(false)
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const callbackUrl = searchParams.get('returnUrl') || searchParams.get('callbackUrl') || '/'

  useEffect(() => {
    if (session) {
      router.push(callbackUrl)
    }
  }, [session, router, callbackUrl])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)

    try {
      if (!email || !password) {
        throw new Error(t('auth.login.error.required'))
      }

      console.log('Attempting to sign in with credentials, callbackUrl:', callbackUrl)
      
      const result = await signIn('credentials', {
        email,
        password,
        redirect: true,
        callbackUrl,
      })

      // 由于设置了 redirect: true,下面的代码不会执行
      // 登录成功后会自动重定向到 callbackUrl
      console.log('Sign in result:', result)
    } catch (error: any) {
      console.error('Sign in error:', error)
      toast.error(error.message || t('auth.signIn.error'))
    } finally {
      setLoading(false)
    }
  }

  const handleGitHubLogin = async () => {
    setLoading(true)
    try {
      console.log('Attempting GitHub login, callbackUrl:', callbackUrl)
      await signIn('github', { 
        callbackUrl,
        redirect: true
      })
    } catch (error) {
      console.error('GitHub login error:', error)
      toast.error(t('auth.signIn.error'))
    }
  }

  const handleGoogleLogin = async () => {
    setLoading(true)
    try {
      console.log('Attempting Google login, callbackUrl:', callbackUrl)
      await signIn('google', { 
        callbackUrl,
        redirect: true
      })
    } catch (error) {
      console.error('Google login error:', error)
      toast.error(t('auth.signIn.error'))
    }
  }

  if (session) {
    return null
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-background">
      <div className="mx-auto max-w-[350px] space-y-6">
        <div className="space-y-2 text-center">
          <h1 className="text-2xl font-bold" suppressHydrationWarning>
            {t('auth.login.title')}
          </h1>
          <p className="text-sm text-muted-foreground" suppressHydrationWarning>
            {t('auth.login.subtitle')}
          </p>
        </div>
        <div className="grid grid-cols-1 gap-2">
          <Button 
            variant="outline" 
            className="w-full" 
            onClick={handleGitHubLogin}
            disabled={loading}
          >
            <Github className="mr-2 h-4 w-4" />
            <span suppressHydrationWarning>{t('auth.login.github')}</span>
          </Button>
          <Button 
            variant="outline" 
            className="w-full" 
            onClick={handleGoogleLogin}
            disabled={loading}
          >
            <svg className="mr-2 h-4 w-4" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="google" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 488 512">
              <path fill="currentColor" d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"></path>
            </svg>
            <span suppressHydrationWarning>{t('auth.login.google')}</span>
          </Button>
        </div>
        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <Separator className="w-full" />
          </div>
          <div className="relative flex justify-center text-xs uppercase">
            <span className="bg-background px-2 text-muted-foreground" suppressHydrationWarning>
              {t('auth.login.or')}
            </span>
          </div>
        </div>
        <form onSubmit={handleSubmit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email" suppressHydrationWarning>
              {t('auth.login.email')}
            </Label>
            <Input
              id="email"
              placeholder={t('auth.login.emailPlaceholder')}
              required
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              disabled={loading}
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password" suppressHydrationWarning>
              {t('auth.login.password')}
            </Label>
            <Input
              id="password"
              placeholder={t('auth.login.passwordPlaceholder')}
              required
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              disabled={loading}
            />
          </div>
          <Button className="w-full" type="submit" disabled={loading}>
            <span suppressHydrationWarning>
              {loading ? t('auth.login.loading') : t('auth.login.button')}
            </span>
          </Button>
        </form>
        <div className="text-center text-sm">
          <span suppressHydrationWarning>{t('auth.login.noAccount')}</span>{' '}
          <Link className="underline" href="/register">
            <span suppressHydrationWarning>{t('auth.signUp')}</span>
          </Link>
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: app/page.tsx
================================================
"use client"

import Link from 'next/link'
import { useI18n } from '@/lib/i18n/use-translations'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { motion } from 'framer-motion'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Autoplay, Navigation, Pagination } from 'swiper/modules'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import { 
  Languages, 
  Image, 
  FileText, 
  Mic, 
  Video, 
  Moon, 
  Lock, 
  Crown, 
  Globe2, 
  Chrome, 
  MonitorSmartphone, 
  ArrowRight, 
  Sparkles 
} from 'lucide-react'

export default function Home() {
  const { t } = useI18n()

  const container = {
    hidden: { opacity: 0 },
    show: {
      opacity: 1,
      transition: {
        staggerChildren: 0.1
      }
    }
  }

  const item = {
    hidden: { opacity: 0, y: 20 },
    show: { opacity: 1, y: 0 }
  }

  const features = [
    {
      icon: <Languages className="h-8 w-8" />,
      title: t('landing.features.text.title'),
      description: t('landing.features.text.description'),
    },
    {
      icon: <Image className="h-8 w-8" />,
      title: t('landing.features.image.title'),
      description: t('landing.features.image.description'),
    },
    {
      icon: <FileText className="h-8 w-8" />,
      title: t('landing.features.pdf.title'),
      description: t('landing.features.pdf.description'),
    },
    {
      icon: <Mic className="h-8 w-8" />,
      title: t('landing.features.speech.title'),
      description: t('landing.features.speech.description'),
    },
    {
      icon: <Video className="h-8 w-8" />,
      title: t('landing.features.video.title'),
      description: t('landing.features.video.description'),
    },
  ]

  const highlights = [
    {
      icon: <Globe2 className="h-6 w-6" />,
      title: t('landing.highlights.multilingual.title'),
      description: t('landing.highlights.multilingual.description'),
    },
    {
      icon: <Moon className="h-6 w-6" />,
      title: t('landing.highlights.theme.title'),
      description: t('landing.highlights.theme.description'),
    },
    {
      icon: <Lock className="h-6 w-6" />,
      title: t('landing.highlights.privacy.title'),
      description: t('landing.highlights.privacy.description'),
    },
    {
      icon: <Chrome className="h-6 w-6" />,
      title: t('landing.highlights.web.title'),
      description: t('landing.highlights.web.description'),
    },
  ]

  const steps = [
    {
      number: "01",
      title: t('landing.steps.select.title'),
      description: t('landing.steps.select.description'),
    },
    {
      number: "02",
      title: t('landing.steps.upload.title'),
      description: t('landing.steps.upload.description'),
    },
    {
      number: "03",
      title: t('landing.steps.translate.title'),
      description: t('landing.steps.translate.description'),
    },
  ]

  const testimonials = [
    {
      quote: t('landing.testimonials.1.quote'),
      author: t('landing.testimonials.1.author'),
      role: t('landing.testimonials.1.role'),
      rating: 5
    },
    {
      quote: t('landing.testimonials.2.quote'),
      author: t('landing.testimonials.2.author'),
      role: t('landing.testimonials.2.role'),
      rating: 5
    },
    {
      quote: t('landing.testimonials.3.quote'),
      author: t('landing.testimonials.3.author'),
      role: t('landing.testimonials.3.role'),
      rating: 5
    },
    {
      quote: t('landing.testimonials.4.quote'),
      author: t('landing.testimonials.4.author'),
      role: t('landing.testimonials.4.role'),
      rating: 5
    },
    {
      quote: t('landing.testimonials.5.quote'),
      author: t('landing.testimonials.5.author'),
      role: t('landing.testimonials.5.role'),
      rating: 5
    },
    {
      quote: t('landing.testimonials.6.quote'),
      author: t('landing.testimonials.6.author'),
      role: t('landing.testimonials.6.role'),
      rating: 5
    }
  ]

  return (
    <div className="flex flex-col items-center">
      {/* Hero Section */}
      <section className="relative w-full py-12 md:py-16 lg:py-20 bg-background overflow-hidden">
        <div className="absolute inset-0">
          <div className="absolute inset-0 bg-grid-slate-400/[0.05] bg-[size:32px_32px] dark:bg-grid-slate-600/[0.05]" />
          <div className="absolute inset-0 flex items-center justify-center bg-background [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]" />
        </div>
        <div className="absolute inset-0 bg-gradient-to-t from-background to-transparent" />
        <motion.div 
          className="container relative px-4 md:px-6"
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5 }}
        >
          <div className="flex flex-col items-center space-y-6 text-center">
            <motion.div 
              className="space-y-4"
              initial={{ opacity: 0, scale: 0.9 }}
              animate={{ opacity: 1, scale: 1 }}
              transition={{ duration: 0.5, delay: 0.2 }}
            >
              <Badge variant="outline" className="px-6 py-2 text-base relative overflow-hidden group mb-4">
                <span className="relative z-10" suppressHydrationWarning>{t('landing.hero.badge')}</span>
                <motion.div
                  className="absolute inset-0 bg-primary/10"
                  initial={{ x: '-100%' }}
                  animate={{ x: '100%' }}
                  transition={{ duration: 1.5, repeat: Infinity, ease: 'linear' }}
                />
              </Badge>
              <h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl/none mb-6">
                <span className="inline-block bg-clip-text text-transparent bg-gradient-to-r from-primary via-primary/50 to-primary bg-[200%_auto] animate-gradient" suppressHydrationWarning>
                  {t('landing.hero.appTitle')}
                </span>
              </h1>
              <p className="mx-auto max-w-[800px] text-gray-500 md:text-xl lg:text-2xl dark:text-gray-400 leading-relaxed" suppressHydrationWarning>
                {t('landing.hero.subtitle')}
              </p>
            </motion.div>
            <motion.div 
              className="flex flex-col sm:flex-row gap-4 mt-8"
              initial={{ opacity: 0, y: 20 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ duration: 0.5, delay: 0.4 }}
            >
              <Link href="/translate" className="w-full sm:w-auto">
                <Button size="lg" variant="outline" className="w-full group relative px-8 py-6 text-lg font-medium overflow-hidden">
                  <div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-primary/5 opacity-0 group-hover:opacity-100 transition-opacity" />
                  <span className="relative flex items-center justify-center">
                    <Languages className="mr-3 h-6 w-6 transition-transform group-hover:scale-110" />
                    <span suppressHydrationWarning>{t('landing.hero.cta')}</span>
                    <ArrowRight className="ml-3 h-6 w-6 transition-transform group-hover:translate-x-1" />
                  </span>
                </Button>
              </Link>
              <Link href="/pricing" className="w-full sm:w-auto">
                <Button size="lg" className="w-full group relative px-8 py-6 text-lg font-medium overflow-hidden">
                  <div className="absolute inset-0 bg-gradient-to-r from-primary/50 to-primary/10 opacity-0 group-hover:opacity-100 transition-opacity" />
                  <span className="relative flex items-center justify-center">
                    <Crown className="mr-3 h-6 w-6 transition-transform group-hover:scale-110 text-yellow-500" />
                    <span suppressHydrationWarning>{t('landing.hero.subscribe')}</span>
                    <ArrowRight className="ml-3 h-6 w-6 transition-transform group-hover:translate-x-1" />
                    <Sparkles className="absolute top-0 right-0 h-4 w-4 text-yellow-400 animate-pulse" />
                  </span>
                </Button>
              </Link>
            </motion.div>
          </div>
        </motion.div>
      </section>

      {/* Features Section */}
      <motion.section 
        className="w-full py-12 bg-primary/5"
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true }}
      >
        <div className="container px-4 md:px-6">
          <motion.div 
            className="text-center mb-8"
            variants={item}
          >
            <div className="inline-flex items-center space-x-2 mb-4">
              <div className="h-px w-8 bg-primary/60" />
              <h2 className="text-2xl font-bold" suppressHydrationWarning>{t('landing.features.title')}</h2>
              <div className="h-px w-8 bg-primary/60" />
            </div>
            <p className="text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm" suppressHydrationWarning>
              {t('landing.features.subtitle')}
            </p>
          </motion.div>
          <div className="grid gap-4 lg:grid-cols-5 md:grid-cols-2">
            {features.map((feature, i) => (
              <motion.div key={i} variants={item}>
                <Card className="relative overflow-hidden group hover:shadow-lg transition-all duration-300 bg-card/50 backdrop-blur-sm border-primary/10 h-[240px] flex flex-col">
                  <CardContent className="p-6 flex flex-col flex-1">
                    <div className="mb-4 rounded-full w-12 h-12 flex items-center justify-center bg-primary/10 group-hover:bg-primary/20 transition-colors relative">
                      <div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/0 animate-spin-slow" />
                      {feature.icon}
                    </div>
                    <h3 className="text-lg font-bold mb-2 group-hover:text-primary transition-colors" suppressHydrationWarning>
                      {feature.title}
                    </h3>
                    <p className="text-gray-500 dark:text-gray-400 text-sm flex-1" suppressHydrationWarning>
                      {feature.description}
                    </p>
                  </CardContent>
                  <div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent transform scale-x-0 group-hover:scale-x-100 transition-transform" />
                </Card>
              </motion.div>
            ))}
          </div>
        </div>
      </motion.section>

      {/* Highlights Section */}
      <motion.section 
        className="w-full py-12"
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true }}
      >
        <div className="container px-4 md:px-6">
          <motion.div 
            className="text-center mb-8"
            variants={item}
          >
            <div className="inline-flex items-center space-x-2 mb-4">
              <div className="h-px w-8 bg-primary/60" />
              <h2 className="text-2xl font-bold" suppressHydrationWarning>{t('landing.highlights.title')}</h2>
              <div className="h-px w-8 bg-primary/60" />
            </div>
            <p className="text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm" suppressHydrationWarning>
              {t('landing.highlights.subtitle')}
            </p>
          </motion.div>
          <div className="grid gap-6 lg:grid-cols-4 md:grid-cols-2">
            {highlights.map((highlight, i) => (
              <motion.div 
                key={i} 
                variants={item}
                className="group"
              >
                <div className="relative p-6 bg-background/50 backdrop-blur-sm rounded-lg border border-primary/10 hover:border-primary/30 transition-colors">
                  <div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-primary/0 rounded-lg" />
                  <div className="relative">
                    <div className="mb-4 p-3 rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors w-12 h-12 flex items-center justify-center">
                      {highlight.icon}
                    </div>
                    <h3 className="text-lg font-bold mb-1 group-hover:text-primary transition-colors" suppressHydrationWarning>
                      {highlight.title}
                    </h3>
                    <p className="text-gray-500 dark:text-gray-400 text-sm" suppressHydrationWarning>
                      {highlight.description}
                    </p>
                  </div>
                </div>
              </motion.div>
            ))}
          </div>
        </div>
      </motion.section>

      {/* Steps Section */}
      <motion.section 
        className="w-full py-12 bg-primary/5"
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true }}
      >
        <div className="container px-4 md:px-6">
          <motion.div 
            className="text-center mb-8"
            variants={item}
          >
            <div className="inline-flex items-center space-x-2 mb-4">
              <div className="h-px w-8 bg-primary/60" />
              <h2 className="text-2xl font-bold" suppressHydrationWarning>{t('landing.steps.title')}</h2>
              <div className="h-px w-8 bg-primary/60" />
            </div>
            <p className="text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm" suppressHydrationWarning>
              {t('landing.steps.subtitle')}
            </p>
          </motion.div>
          <div className="grid gap-6 lg:grid-cols-3">
            {steps.map((step, i) => (
              <motion.div 
                key={i} 
                variants={item}
                className="relative flex flex-col items-center text-center group"
              >
                <div className="mb-4">
                  <div className="relative">
                    <div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center text-xl font-bold text-primary relative overflow-hidden group-hover:bg-primary/20 transition-colors">
                      <div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/0 animate-spin-slow" />
                      <span className="relative">{step.number}</span>
                    </div>
                    {i < steps.length - 1 && (
                      <div className="absolute top-1/2 left-full w-full h-px bg-gradient-to-r from-primary/40 to-primary/0 -translate-y-1/2 hidden lg:block" />
                    )}
                  </div>
                </div>
                <h3 className="text-lg font-bold mb-1 group-hover:text-primary transition-colors" suppressHydrationWarning>
                  {step.title}
                </h3>
                <p className="text-gray-500 dark:text-gray-400 text-sm" suppressHydrationWarning>
                  {step.description}
                </p>
              </motion.div>
            ))}
          </div>
        </div>
      </motion.section>

      {/* Testimonials Section */}
      <motion.section 
        className="w-full py-12"
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true }}
      >
        <div className="container px-4 md:px-6">
          <motion.div 
            className="text-center mb-8"
            variants={item}
          >
            <div className="inline-flex items-center space-x-2 mb-4">
              <div className="h-px w-8 bg-primary/60" />
              <h2 className="text-2xl font-bold" suppressHydrationWarning>{t('landing.testimonials.title')}</h2>
              <div className="h-px w-8 bg-primary/60" />
            </div>
            <p className="text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm" suppressHydrationWarning>
              {t('landing.testimonials.subtitle')}
            </p>
          </motion.div>
          <div className="relative overflow-hidden">
            <Swiper
              modules={[Autoplay, Navigation, Pagination]}
              spaceBetween={30}
              slidesPerView={1}
              breakpoints={{
                640: {
                  slidesPerView: 2,
                },
                1024: {
                  slidesPerView: 3,
                },
              }}
              autoplay={{
                delay: 3000,
                disableOnInteraction: false,
              }}
              pagination={{
                clickable: true,
              }}
              navigation
              loop
              className="testimonials-swiper"
            >
              {testimonials.map((testimonial, i) => (
                <SwiperSlide key={i}>
                  <Card className="group bg-background/50 backdrop-blur-sm hover:shadow-lg transition-all duration-300 border-primary/10 hover:border-primary/30 h-[280px] flex flex-col">
                    <CardContent className="p-6 relative flex flex-col flex-1">
                      <div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-br from-primary/20 to-transparent rounded-bl-full" />
                      <div className="mb-3">
                        {[...Array(testimonial.rating)].map((_, i) => (
                          <span key={i} className="text-yellow-400 animate-pulse">★</span>
                        ))}
                      </div>
                      <p className="mb-4 text-base italic text-gray-600 dark:text-gray-300 relative flex-1" suppressHydrationWarning>
                        <span className="absolute -top-2 -left-2 text-4xl text-primary/20">"</span>
                        {testimonial.quote}
                        <span className="absolute -bottom-4 -right-2 text-4xl text-primary/20">"</span>
                      </p>
                      <div className="flex items-center space-x-3 mt-auto">
                        <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-sm relative overflow-hidden group-hover:bg-primary/20 transition-colors">
                          <div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/0 animate-spin-slow" />
                          <span className="relative" suppressHydrationWarning>{testimonial.author[0]}</span>
                        </div>
                        <div>
                          <p className="font-bold text-sm" suppressHydrationWarning>{testimonial.author}</p>
                          <p className="text-xs text-gray-500 dark:text-gray-400" suppressHydrationWarning>{testimonial.role}</p>
                        </div>
                      </div>
                    </CardContent>
                  </Card>
                </SwiperSlide>
              ))}
            </Swiper>
          </div>
        </div>
      </motion.section>
    </div>
  )
}

================================================
FILE: app/pricing/layout.tsx
================================================
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Pricing Plans - AI Translation Assistant',
  description: 'Affordable pricing plans for AI-powered translation services. Choose from Free, Pro, and Enterprise plans with various features and usage limits.',
  alternates: {
    canonical: '/pricing',
  },
  openGraph: {
    title: 'Pricing Plans - AI Translation Assistant',
    description: 'Affordable pricing plans for AI-powered translation services. Choose from Free, Pro, and Enterprise plans with various features and usage limits.',
    url: '/pricing',
    images: [
      {
        url: '/og-image.png',
        width: 1200,
        height: 630,
        alt: 'AI Translation Assistant - Pricing Page',
      },
    ],
  },
}

export default function PricingLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return children
}


================================================
FILE: app/pricing/page.tsx
================================================
'use client'

import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Check, Minus, Clock, RefreshCw, CreditCard, Users, MessageCircle, Shield } from "lucide-react"
import { cn } from "@/lib/utils"
import Link from "next/link"
import { useLanguage } from "@/components/language-provider"
import { useMemo, useState } from "react"
import { useSession } from "next-auth/react"
import { toast } from "sonner"
import { PLANS } from "@/lib/stripe"

export default function PricingPage() {
  const { translations: t } = useLanguage()
  const { data: session } = useSession()
  const [isLoading, setIsLoading] = useState<string | null>(null)
  
  if (!t?.pricing) {
    return null
  }

  const pricing = t.pricing as NonNullable<typeof t.pricing>

  const handleSubscribe = async (priceId: string) => {
    try {
      console.log('Subscribing with priceId:', priceId)
      console.log('Session status:', session)
      setIsLoading(priceId)
      
      if (!session) {
        console.log('No session found, redirecting to login')
        toast.error(t.auth.error.notLoggedIn)
        window.location.href = `/login?callbackUrl=${encodeURIComponent('/pricing')}`
        return
      }

      console.log('User is logged in, proceeding with subscription')
      console.log('Making API request to /api/subscription')
      const response = await fetch('/api/subscription', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          priceId,
        }),
      })

      console.log('API Response:', response)
      const data = await response.json()
      console.log('API Data:', data)
      
      if (!response.ok) {
        throw new Error(data.message)
      }

      // 重定向到 Stripe Checkout
      console.log('Redirecting to:', data.url)
      window.location.href = data.url
    } catch (error) {
      console.error('Subscription error:', error)
      toast.error(t.error.default)
    } finally {
      setIsLoading(null)
    }
  }

  const tiers = useMemo(() => {
    console.log('PLANS:', PLANS)
    console.log('Monthly Price ID:', PLANS.monthly.priceId)
    console.log('Yearly Price ID:', PLANS.yearly.priceId)
    console.log('Env Monthly ID:', process.env.STRIPE_MONTHLY_PRICE_ID)
    console.log('Env Yearly ID:', process.env.STRIPE_YEARLY_PRICE_ID)
    return [
    {
      id: "free" as const,
      price: "$0",
      features: pricing.tiers.free.features
    },
    {
      id: "yearly" as const,
      price: "$99.99",
      priceId: PLANS.yearly.priceId,
      isRecommended: true,
      features: pricing.tiers.yearly.features
    },
    {
      id: "monthly" as const,
      price: "$9.99",
      priceId: PLANS.monthly.priceId,
      features: pricing.tiers.monthly.features
    }
  ]}, [pricing])

  const renderFeatureSection = (features: string[], title: string, tierId?: string) => {
    // 为免费版使用特殊的键名
    const sectionTitle = tierId === 'free' && (title === 'advanced' || title === 'support') 
      ? `free${title.charAt(0).toUpperCase() + title.slice(1)}` 
      : title;
    
    return (
      <div className="mb-6">
        <h4 className="text-sm font-medium text-muted-foreground mb-3">{pricing.features[title]}</h4>
        <ul className="space-y-3">
          {features.map((feature) => (
            <li key={feature} className="flex items-center gap-2">
              <Check className="h-5 w-5 text-primary flex-shrink-0" />
              <span>{feature}</span>
            </li>
          ))}
        </ul>
      </div>
    )
  }

  return (
    <div className="container py-20">
      <div className="text-center mb-12">
        <h1 className="text-4xl font-bold mb-4">{pricing.title}</h1>
        <p className="text-xl text-muted-foreground">
          {pricing.subtitle}
        </p>
      </div>

      {/* 桌面版定价方案 */}
      <div className="hidden md:grid md:grid-cols-3 gap-8">
        {tiers.map((tier) => (
          <Card 
            key={tier.id}
            className={cn(
              "relative p-8 rounded-lg border",
              tier.isRecommended && "border-2 border-primary shadow-lg"
            )}
          >
            {tier.isRecommended && (
              <div className="absolute -top-4 left-1/2 -translate-x-1/2">
                <span className="bg-primary text-primary-foreground text-sm font-medium px-3 py-1 rounded-full">
                  {pricing.tiers.yearly.recommended}
                </span>
              </div>
            )}

            <div className="mb-8">
              <h2 className="text-2xl font-bold mb-2">{pricing.tiers[tier.id].name}</h2>
              <p className="text-muted-foreground mb-4">{pricing.tiers[tier.id].description}</p>
              <div className="flex items-baseline gap-1">
                <span className="text-4xl font-bold">{tier.price}</span>
                {tier.id !== "free" && (
                  <span className="text-muted-foreground">
                    /{pricing.tiers[tier.id].name}
                  </span>
                )}
                {tier.id === "yearly" && (
                  <span className="ml-2 inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/10 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/10">
                    {pricing.tiers.yearly.discount}
                  </span>
                )}
              </div>
            </div>

            <div className="space-y-6 mb-8">
              {renderFeatureSection(tier.features.basic, 'basic', tier.id)}
              {renderFeatureSection(tier.features.advanced || tier.features.freeAdvanced, 'advanced', tier.id)}
              {renderFeatureSection(tier.features.support || tier.features.freeSupport, 'support', tier.id)}
            </div>

            {tier.id === "free" ? (
              <Link href="/translate" className="w-full">
                <Button className="w-full" variant="outline">
                  {pricing.tiers.free.cta}
                </Button>
              </Link>
            ) : (
              <Button 
                className="w-full"
                variant={tier.isRecommended ? "default" : "outline"}
                onClick={() => handleSubscribe(tier.priceId!)}
                disabled={isLoading === tier.priceId}
              >
                {isLoading === tier.priceId ? t.loading : pricing.tiers[tier.id].cta}
              </Button>
            )}
          </Card>
        ))}
      </div>

      {/* 移动端定价方案 */}
      <div className="space-y-6 md:hidden">
        {tiers.map((tier) => (
          <Card 
            key={tier.id}
            className={cn(
              "p-6",
              tier.isRecommended && "border-2 border-primary"
            )}
          >
            <div className="flex justify-between items-center mb-6">
              <div>
                <h3 className="text-lg font-semibold">{pricing.tiers[tier.id].name}</h3>
                <p className="text-sm text-muted-foreground mt-1">{pricing.tiers[tier.id].description}</p>
              </div>
              <div className="text-right">
                <span className="text-2xl font-bold">{tier.price}</span>
                {tier.id !== "free" && (
                  <div className="text-sm text-muted-foreground">
                    /{pricing.tiers[tier.id].name}
                  </div>
                )}
                {tier.id === "yearly" && (
                  <span className="mt-1 inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/10 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/10">
                    {pricing.tiers.yearly.discount}
                  </span>
                )}
              </div>
            </div>
            <div className="space-y-4 mb-6">
              {renderFeatureSection(tier.features.basic, 'basic', tier.id)}
              {renderFeatureSection(tier.features.advanced || tier.features.freeAdvanced, 'advanced', tier.id)}
              {renderFeatureSection(tier.features.support || tier.features.freeSupport, 'support', tier.id)}
            </div>
            {tier.id === "free" ? (
              <Link href="/translate" className="w-full">
                <Button className="w-full" variant="outline">
                  {pricing.tiers.free.cta}
                </Button>
              </Link>
            ) : (
              <Button 
                className="w-full" 
                variant="outline"
                onClick={() => handleSubscribe(tier.priceId!)}
                disabled={isLoading === tier.priceId}
              >
                {isLoading === tier.priceId ? t.loading : pricing.tiers[tier.id].cta}
              </Button>
            )}
          </Card>
        ))}
      </div>

      {/* FAQ部分 */}
      <div className="mt-24 max-w-4xl mx-auto">
        <div className="text-center mb-12">
          <h3 className="text-3xl font-bold mb-4">{pricing.faq.title}</h3>
          <p className="text-muted-foreground">
            {pricing.faq.subtitle}
          </p>
        </div>

        <div className="grid md:grid-cols-2 gap-6">
          {Object.entries({
            security: { icon: Shield },
            quota: { icon: Clock },
            payment: { icon: CreditCard },
            support: { icon: MessageCircle }
          }).map(([key, { icon: Icon }]) => (
            <Card key={key} className="p-6 hover:shadow-lg transition-shadow">
              <h4 className="flex items-center gap-2 text-lg font-semibold mb-3">
                <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
                  <Icon className="h-4 w-4 text-primary" />
                </div>
                {pricing.faq.cards[key].title}
              </h4>
              <p className="text-muted-foreground">
                {pricing.faq.cards[key].content}
              </p>
            </Card>
          ))}
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: app/profile/page.tsx
================================================
"use client";

import { useEffect, useState } from "react"
import { useSession, signIn } from "next-auth/react"
import { useRouter, useSearchParams } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { useI18n } from "@/lib/i18n/use-translations"
import { useLanguage } from "@/components/language-provider"
import { Progress } from "@/components/ui/progress"
import { toast } from 'sonner'
import confetti from 'canvas-confetti'
import { PLANS } from '@/lib/stripe'
import { Badge } from '@/components/ui/badge'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Pencil } from "lucide-react"

interface UserInfo {
  user: {
    id: string;
    email: string;
    name: string | null;
    created_at: string;
    updated_at: string;
  };
  subscription: {
    stripe_customer_id: string | null;
    stripe_subscription_id: string | null;
    stripe_price_id: string | null;
    stripe_current_period_end: string | null;
  };
  quota: {
    text_quota: number;
    image_quota: number;
    pdf_quota: number;
    speech_quota: number;
    video_quota: number;
  };
  usage: {
    text: number;
    image: number;
    pdf: number;
    speech: number;
    video: number;
  };
}

const QUOTA_TYPES = ['text', 'image', 'pdf', 'speech', 'video'] as const

export default function ProfilePage() {
  const { data: session, status } = useSession()
  const router = useRouter()
  const { t } = useI18n()
  const { language } = useLanguage()
  const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
  const searchParams = useSearchParams()
  const [isEditingName, setIsEditingName] = useState(false)
  const [newName, setNewName] = useState('')
  const [isUpdating, setIsUpdating] = useState(false)

  // 获取用户信息的函数
  const fetchUserInfo = async () => {
    try {
      console.log('开始获取用户信息')
      const response = await fetch('/api/user/info')
      const data = await response.json()
      if (response.ok) {
        console.log('获取到的用户信息:', {
          id: data.user.id,
          email: data.user.email,
          subscription: {
            customerId: data.subscription.stripe_customer_id,
            subscriptionId: data.subscription.stripe_subscription_id,
            priceId: data.subscription.stripe_price_id,
            currentPeriodEnd: data.subscription.stripe_current_period_end
          },
          quota: data.quota,
          usage: data.usage
        })
        setUserInfo(data)
      } else {
        console.error('获取用户信息失败:', data.error)
      }
    } catch (error) {
      console.error('获取用户信息失败:', error)
    }
  }

  useEffect(() => {
    // 如果已经确认未登录,则重定向到登录页面
    if (status === 'unauthenticated') {
      console.log('用户未登录,重定向到登录页面')
      router.push('/login?callbackUrl=/profile')
      return
    }

    // 如果已登录,获取用户信息
    if (status === 'authenticated') {
      console.log('用户已登录,获取用户信息')
      // 只在页面加载时获取一次用户信息
      fetchUserInfo()
    }
  }, [status, router, t])

  // 订阅成功效果
  useEffect(() => {
    if (userInfo && searchParams.get('subscription') === 'success') {
      console.log('用户信息加载完成,检测到订阅成功参数,显示成功提示')
      toast.success(t('auth.profile.subscription.success'))
      confetti({
        particleCount: 100,
        spread: 70,
        origin: { y: 0.6 }
      })
      
      // 清除 URL 中的 subscription 参数,防止刷新页面时重复显示
      const newUrl = new URL(window.location.href)
      newUrl.searchParams.delete('subscription')
      router.replace(newUrl.pathname + newUrl.search)
    }
  }, [userInfo, searchParams, t, router])

  // 如果正在检查登录状态,显示加载状态
  if (status === 'loading') {
    return (
      <div className="container py-20">
        <div className="flex items-center justify-center min-h-[200px]">
          <div className="text-muted-foreground">Loading...</div>
        </div>
      </div>
    )
  }

  // 如果未登录,返回 null(会被上面的 useEffect 重定向)
  if (!session) return null

  // 如果已登录但还没有获取到用户信息,显示加载状态
  if (!userInfo) {
    return (
      <div className="container py-20">
        <div className="flex items-center justify-center min-h-[200px]">
          <div className="text-muted-foreground">Loading...</div>
        </div>
      </div>
    )
  }

  // 更新用户名
  const updateName = async () => {
    if (!newName.trim()) {
      toast.error(t('auth.profile.basicInfo.editUsername.error.empty'))
      return
    }

    try {
      setIsUpdating(true)
      const response = await fetch('/api/user/update', {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name: newName.trim() }),
      })

      if (response.ok) {
        toast.success(t('auth.profile.basicInfo.editUsername.success'))
        setIsEditingName(false)
        fetchUserInfo()
      } else {
        const data = await response.json()
        toast.error(data.error || t('auth.profile.basicInfo.editUsername.error.failed'))
      }
    } catch (error) {
      console.error('更新用户名失败:', error)
      toast.error(t('auth.profile.basicInfo.editUsername.error.failed'))
    } finally {
      setIsUpdating(false)
    }
  }

  const renderQuotaItem = (type: typeof QUOTA_TYPES[number]) => {
    const quota = userInfo.quota[`${type}_quota` as keyof typeof userInfo.quota]
    const used = userInfo.usage[type]
    const percentage = quota === -1 ? 0 : (used / quota) * 100

    return (
      <div className="p-4 bg-muted rounded-lg">
        <div className="text-sm text-muted-foreground mb-1">
          {t(`auth.profile.subscription.quota.${type}`)}
        </div>
        <div className="text-2xl font-semibold mb-2">
          {quota === -1 ? 
            t('auth.profile.subscription.quota.unlimited') : 
            t('auth.profile.subscription.quota.usage', { used: used.toString(), quota: quota.toString() })
          }
        </div>
        {quota !== -1 && (
          <Progress value={percentage} className="h-2" />
        )}
      </div>
    )
  }

  const getPlanName = () => {
    if (!userInfo.subscription.stripe_price_id) return t('auth.profile.subscription.plan.trial')
    if (userInfo.subscription.stripe_price_id === PLANS.monthly.priceId) return t('auth.profile.subscription.plan.monthly')
    if (userInfo.subscription.stripe_price_id === PLANS.yearly.priceId) return t('auth.profile.subscription.plan.yearly')
    return t('auth.profile.subscription.plan.trial')
  }

  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    if (language === 'zh') {
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
    }
    return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
  }

  const renderNameField = () => {
    if (isEditingName) {
      return (
        <div className="flex items-center space-x-2">
          <Input
            value={newName}
            onChange={(e) => setNewName(e.target.value)}
            placeholder={t('auth.profile.basicInfo.editUsername.placeholder')}
            maxLength={50}
            className="max-w-[200px]"
          />
          <Button 
            onClick={updateName} 
            disabled={isUpdating}
            size="sm"
          >
            {t('auth.profile.basicInfo.editUsername.save')}
          </Button>
          <Button 
            onClick={() => setIsEditingName(false)}
            variant="outline"
            size="sm"
          >
            {t('auth.profile.basicInfo.editUsername.cancel')}
          </Button>
        </div>
      )
    }

    return (
      <div className="flex items-center space-x-2">
        <div className="text-lg font-medium">
          {userInfo?.user.name || '-'}
        </div>
        <Button
          variant="ghost"
          size="icon"
          className="h-8 w-8"
          onClick={() => {
            setNewName(userInfo?.user.name || '')
            setIsEditingName(true)
          }}
        >
          <Pencil className="h-4 w-4" />
        </Button>
      </div>
    )
  }

  return (
    <div className="container py-20">
      <h1 className="text-4xl font-bold mb-8">{t('auth.profile.title')}</h1>
      <div className="space-y-6">
        {/* 基本信息卡片 */}
        <Card className="p-6">
          <div className="flex items-center justify-between mb-4">
            <h2 className="text-2xl font-semibold">{t('auth.profile.basicInfo.title')}</h2>
          </div>
          <div className="space-y-4">
            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
              <div className="p-4 bg-muted rounded-lg">
                <div className="text-sm text-muted-foreground mb-1">{t('auth.profile.basicInfo.email')}</div>
                <div className="text-lg font-medium">
                  {userInfo?.user.email || '-'}
                </div>
              </div>
              <div className="p-4 bg-muted rounded-lg">
                <div className="text-sm text-muted-foreground mb-1">{t('auth.profile.basicInfo.username')}</div>
                {renderNameField()}
              </div>
              <div className="p-4 bg-muted rounded-lg">
                <div className="text-sm text-muted-foreground mb-1">{t('auth.profile.basicInfo.userId')}</div>
                <div className="text-lg font-medium">
                  {userInfo?.user.id || '-'}
                </div>
              </div>
              <div className="p-4 bg-muted rounded-lg">
                <div className="text-sm text-muted-foreground mb-1">{t('auth.profile.basicInfo.registerTime')}</div>
                <div className="text-lg font-medium">
                  {userInfo?.user.created_at ? formatDate(userInfo.user.created_at) : '-'}
                </div>
              </div>
            </div>
          </div>
        </Card>

        {/* 订阅信息卡片 */}
        <Card className="p-6">
          <div className="flex items-center justify-between mb-4">
            <h2 className="text-2xl font-semibold">{t('auth.profile.subscription.title')}</h2>
            <Badge variant={userInfo.subscription.stripe_price_id ? "default" : "secondary"}>
              {getPlanName()}
            </Badge>
          </div>
          
          <div className="space-y-4">
            {userInfo.subscription.stripe_customer_id && (
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
                <div className="p-4 bg-muted rounded-lg">
                  <div className="text-sm text-muted-foreground mb-1">{t('auth.profile.subscription.customerId')}</div>
                  <div className="text-lg font-medium">
                    {userInfo.subscription.stripe_customer_id}
                  </div>
                </div>
                <div className="p-4 bg-muted rounded-lg">
                  <div className="text-sm text-muted-foreground mb-1">{t('auth.profile.subscription.subscriptionId')}</div>
                  <div className="text-lg font-medium">
                    {userInfo.subscription.stripe_subscription_id || '-'}
                  </div>
                </div>
              </div>
            )}

            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
              {QUOTA_TYPES.map(type => (
                <div key={type}>
                  {renderQuotaItem(type)}
                </div>
              ))}
            </div>

            {userInfo.subscription.stripe_current_period_end && (
              <div className="text-sm text-muted-foreground">
                {t('auth.profile.subscription.expiryDate', { 
                  date: formatDate(userInfo.subscription.stripe_current_period_end)
                })}
              </div>
            )}
          </div>
        </Card>
      </div>
    </div>
  )
} 

================================================
FILE: app/providers.tsx
================================================
"use client";

import { SessionProvider } from "next-auth/react";
import { useLanguage } from "@/components/language-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { Header } from '@/components/header';
import { Footer } from '@/components/footer';
import { Toaster } from '@/components/ui/toaster';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      <ThemeProvider
        attribute="class"
        defaultTheme="system"
        enableSystem
        disableTransitionOnChange
      >
        <div className="min-h-screen flex flex-col">
          <Header />
          <main className="container mx-auto px-4 py-8 flex-1">
            {children}
          </main>
          <Footer />
        </div>
        <Toaster />
      </ThemeProvider>
    </SessionProvider>
  );
} 

================================================
FILE: app/register/layout.tsx
================================================
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Sign up',
  description: 'Create your account',
}

export default function RegisterLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return children
} 

================================================
FILE: app/register/page.tsx
================================================
"use client";

import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { toast } from 'sonner'
import { useI18n } from '@/lib/i18n/use-translations'
import { useSession, signIn } from 'next-auth/react'
import { Github } from 'lucide-react'
import { Separator } from '@/components/ui/separator'

export default function RegisterPage() {
  const { t } = useI18n()
  const router = useRouter()
  const searchParams = useSearchParams()
  const { data: session } = useSession()
  const [loading, setLoading] = useState(false)
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')

  const callbackUrl = searchParams.get('returnUrl') || searchParams.get('callbackUrl') || '/'

  useEffect(() => {
    if (session) {
      router.push(callbackUrl)
    }
  }, [session, router, callbackUrl])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)

    try {
      if (!email || !password || !confirmPassword) {
        throw new Error(t('auth.register.error.required'))
      }

      if (password !== confirmPassword) {
        throw new Error(t('auth.register.error.passwordMismatch'))
      }

      console.log('Attempting to register, callbackUrl:', callbackUrl)

      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email,
          password,
        }),
      })

      const data = await response.json()

      if (!response.ok) {
        throw new Error(data.error || t('auth.register.error.emailExists'))
      }

      toast.success(t('auth.register.success'))
      
      console.log('Registration successful, attempting to sign in')
      
      await signIn('credentials', {
        email,
        password,
        redirect: true,
        callbackUrl,
      })

      // 由于设置了 redirect: true,下面的代码不会执行
      console.log('Sign in after registration successful')
    } catch (error: any) {
      console.error('Registration/sign in error:', error)
      toast.error(error.message || t('auth.register.error'))
    } finally {
      setLoading(false)
    }
  }

  const handleGitHubLogin = async () => {
    setLoading(true)
    try {
      console.log('Attempting GitHub login from register page, callbackUrl:', callbackUrl)
      await signIn('github', { 
        callbackUrl,
        redirect: true
      })
    } catch (error) {
      console.error('GitHub login error:', error)
      toast.error(t('auth.signIn.error'))
    }
  }

  const handleGoogleLogin = async () => {
    setLoading(true)
    try {
      console.log('Attempting Google login from register page, callbackUrl:', callbackUrl)
      await signIn('google', { 
        callbackUrl,
        redirect: true
      })
    } catch (error) {
      console.error('Google login error:', error)
      toast.error(t('auth.signIn.error'))
    }
  }

  if (session) {
    return null
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-background">
      <div className="mx-auto max-w-[350px] space-y-6">
        <div className="space-y-2 text-center">
          <h1 className="text-2xl font-bold" suppressHydrationWarning>
            {t('auth.register.title')}
          </h1>
          <p className="text-sm text-muted-foreground" suppressHydrationWarning>
            {t('auth.register.subtitle')}
          </p>
        </div>
        <div className="grid grid-cols-1 gap-2">
          <Button 
            variant="outline" 
            className="w-full" 
            onClick={handleGitHubLogin}
            disabled={loading}
          >
            <Github className="mr-2 h-4 w-4" />
            <span suppressHydrationWarning>{t('auth.register.github')}</span>
          </Button>
          <Button 
            variant="outline" 
            className="w-full" 
            onClick={handleGoogleLogin}
            disabled={loading}
          >
            <svg className="mr-2 h-4 w-4" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="google" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 488 512">
              <path fill="currentColor" d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"></path>
            </svg>
            <span suppressHydrationWarning>{t('auth.register.google')}</span>
          </Button>
        </div>
        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <Separator className="w-full" />
          </div>
          <div className="relative flex justify-center text-xs uppercase">
            <span className="bg-background px-2 text-muted-foreground" suppressHydrationWarning>
              {t('auth.register.or')}
            </span>
          </div>
        </div>
        <form onSubmit={handleSubmit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email" suppressHydrationWarning>
              {t('auth.register.email')}
            </Label>
            <Input
              id="email"
              placeholder={t('auth.register.emailPlaceholder')}
              required
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              disabled={loading}
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password" suppressHydrationWarning>
              {t('auth.register.password')}
            </Label>
            <Input
              id="password"
              placeholder={t('auth.register.passwordPlaceholder')}
              required
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              disabled={loading}
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="confirmPassword" suppressHydrationWarning>
              {t('auth.register.confirmPassword')}
            </Label>
            <Input
              id="confirmPassword"
              placeholder={t('auth.register.confirmPasswordPlaceholder')}
              required
              type="password"
              value={confirmPassword}
              onChange={(e) => setConfirmPassword(e.target.value)}
              disabled={loading}
            />
          </div>
          <Button className="w-full" type="submit" disabled={loading}>
            <span suppressHydrationWarning>
              {loading ? t('auth.register.loading') : t('auth.register.button')}
            </span>
          </Button>
        </form>
        <div className="text-center text-sm">
          <span suppressHydrationWarning>{t('auth.register.hasAccount')}</span>{' '}
          <Link className="underline" href="/login">
            <span suppressHydrationWarning>{t('auth.signIn')}</span>
          </Link>
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: app/translate/layout.tsx
================================================
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Online Translation - AI Translation Assistant',
  description: 'High-quality multilingual translation powered by AI, supporting text, image, PDF, speech, and video formats.',
  alternates: {
    canonical: '/translate',
  },
  openGraph: {
    title: 'Online Translation - AI Translation Assistant',
    description: 'High-quality multilingual translation powered by AI, supporting text, image, PDF, speech, and video formats.',
    url: '/translate',
    images: [
      {
        url: '/og-image.png',
        width: 1200,
        height: 630,
        alt: 'AI Translation Assistant - Translation Page',
      },
    ],
  },
}

export default function TranslateLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return children
} 

================================================
FILE: app/translate/page.tsx
================================================
"use client"

import { useState, useCallback, useEffect, useRef } from 'react'
import { Upload, Image as ImageIcon, Languages, Wand2, Mic, MicOff, Video, Loader2, FileText, FileType, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useToast } from '@/components/ui/use-toast'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'
import { extractTextFromImage, translateText, improveText } from '@/lib/gemini'
import { extractTextWithTencent } from '@/lib/tencent'
import { getLanguageCategories, getLanguagesByCategory } from '@/lib/languages'
import { useI18n } from '@/lib/i18n/use-translations'
import { TencentASRService } from '@/lib/tencent-asr'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import { translateWithDeepSeek, translateWithQwen, translateWithZhipu, translateWithHunyuan, translateWith4oMini, translateWithMinniMax, translateWithSiliconFlow, translateWithClaude, translateWithStepAPI } from '@/lib/server/translate'
import { extractTextWithQwen } from '@/lib/qwen'
import { extractTextWithGemini } from '@/lib/gemini'
import { extractVideoFrames, analyzeVideoContent, extractTextWithZhipu, extractFileContent } from '@/lib/zhipu'
import { cn } from '@/lib/utils'
import { Textarea } from '@/components/ui/textarea'
import { extractTextWithDeepseek } from '@/lib/deepseek'
import { extractPDFWithKimi, extractPDFContent, extractTextWithKimi } from '@/lib/kimi'
import { useLanguage } from "@/components/language-provider"
import { useAnalytics } from '@/lib/hooks/use-analytics'
import { uploadToOSS } from '@/lib/aliyun-oss-client'
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { extractTextWithStep } from '@/lib/step'
import { SubscriptionDialog } from "@/components/subscription-dialog"

interface QuotaInfo {
  text_quota: number;
  image_quota: number;
  pdf_quota: number;
  speech_quota: number;
  video_quota: number;
  usage: {
    text: number;
    image: number;
    pdf: number;
    speech: number;
    video: number;
  };
}

export default function TranslatePage() {
  const { data: session } = useSession()
  const router = useRouter()
  const { toast } = useToast()
  const { t } = useI18n()
  const { language } = useLanguage()
  const [mounted, setMounted] = useState(false)
  const [image, setImage] = useState<string | null>(null)
  const [extractedText, setExtractedText] = useState('')
  const [translatedText, setTranslatedText] = useState('')
  const [selectedLanguage, setSelectedLanguage] = useState<string>('')
  const [isProcessing, setIsProcessing] = useState(false)
  const [isDragging, setIsDragging] = useState(false)
  const [isListening, setIsListening] = useState(false)
  const [interimText, setInterimText] = useState('')
  const [activeTab, setActiveTab] = useState('text')
  const [asrService, setAsrService] = useState('tencent')
  const asrServiceRef = useRef<TencentASRService | null>(null)
  const recognition = useRef<any>(null)
  const [translationService, setTranslationService] = useState('deepseek')
  const [ocrService, setOcrService] = useState('qwen')
  const [sourceText, setSourceText] = useState('')
  const [videoFile, setVideoFile] = useState<File | null>(null);
  const [pdfFile, setPdfFile] = useState<File | null>(null);
  const [pdfPreview, setPdfPreview] = useState<string | null>(null);
  const [fileContent, setFileContent] = useState<string>('')
  const [isFileProcessing, setIsFileProcessing] = useState(false)
  const [fileService, setFileService] = useState('mistral') // 默认使用 Mistral OCR
  const [videoService, setVideoService] = useState('zhipu')
  const { trackEvent } = useAnalytics()
  const [showAuthDialog, setShowAuthDialog] = useState(false)
  const [quotaInfo, setQuotaInfo] = useState<QuotaInfo | null>(null)
  const [videoContent, setVideoContent] = useState<string>('');
  const [showSubscriptionDialog, setShowSubscriptionDialog] = useState(false)

  // 检查是否还有剩余配额
  const hasRemainingQuota = useCallback((type: keyof Omit<QuotaInfo['usage'], 'text'>) => {
    if (!quotaInfo) return false
    const quotaKey = `${type}_quota` as keyof QuotaInfo
    const quota = quotaInfo[quotaKey]
    const used = quotaInfo.usage[type]
    if (typeof quota === 'number' && typeof used === 'number') {
      return quota === -1 || quota > used
    }
    return false
  }, [quotaInfo])

  // 获取配额信息的函数
  const fetchQuotaInfo = async () => {
    try {
      const response = await fetch('/api/user/info')
      const data = await response.json()
      if (data.error) {
        console.error(t('console.quotaFetchFailed'), t(data.error))
        return
      }
      // 设置配额信息
      setQuotaInfo({
        text_quota: data.quota.text_quota,
        image_quota: data.quota.image_quota,
        pdf_quota: data.quota.pdf_quota,
        speech_quota: data.quota.speech_quota,
        video_quota: data.quota.video_quota,
        usage: data.usage
      })
    } catch (error) {
      console.error(t('console.quotaInfoFetchFailed'), error)
    }
  }

  useEffect(() => {
    if (session) {
      // 初始获取配额信息
      fetchQuotaInfo()
    }
  }, [session])

  // 检查并更新使用次数
  const checkAndUpdateUsage = useCallback(async (type: 'image' | 'pdf' | 'speech' | 'video') => {
    if (!session) {
      setShowAuthDialog(true)
      return false
    }

    try {
      const response = await fetch('/api/user/usage', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ type })
      })

      const data = await response.json()

      if (!response.ok) {
        toast({
          title: t('error.usageLimitExceeded'),
          description: t(`error.${type}LimitExceededDesc`),
          variant: "destructive"
        })
        return false
      }

      // 刷新配额信息
      fetchQuotaInfo()
      return true
    } catch (error) {
      console.error(t('console.usageUpdateFailed'), error)
      return false
    }
  }, [session, t, toast])

  // 获取使用次数显示文本
  const getRemainingUsageText = (type: keyof Omit<QuotaInfo['usage'], 'text'>) => {
    if (!session) {
      return t('usage.loginToGet')
    }
    if (!quotaInfo) {
      return ''
    }
    const quota = quotaInfo[`${type}_quota` as keyof QuotaInfo] as number
    const used = quotaInfo.usage[type] as number
    const remaining = quota - used
    return t('usage.remainingToday', [remaining, quota])
  }

  // 获取文本翻译使用次数显示文本
  const getTextUsageText = () => {
    if (!session) {
      return t('usage.loginToGet')
    }
    return t('usage.unlimited')
  }

  // 检查登录状态的函数
  const checkAuth = useCallback(() => {
    if (!session) {
      setShowAuthDialog(true)
      return false
    }
    return true
  }, [session])

  // 处理登录按钮点击
  const handleLogin = useCallback(() => {
    setShowAuthDialog(false)
    router.push('/login?callbackUrl=/translate')
  }, [router])

  // 处理注册按钮点击
  const handleRegister = useCallback(() => {
    setShowAuthDialog(false)
    router.push('/register?callbackUrl=/translate')
  }, [router])

  // 添加使用次数状态
  const [usageCounts, setUsageCounts] = useState({
    image: 0,
    pdf: 0,
    speech: 0,
    video: 0
  });

  useEffect(() => {
    // 初始化腾讯云语音识别服务
    asrServiceRef.current = new TencentASRService();
  }, []);

  // 处理图片上传
  const handleImageUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!checkAuth()) return;

    // 检查配额
    if (!hasRemainingQuota('image')) {
      setShowSubscriptionDialog(true);
      return;
    }

    const file = e.target.files?.[0];
    if (!file || !file.type.startsWith('image/')) {
      toast({
        title: t('error.invalidImageFile'),
        description: t('error.invalidImageFileDesc'),
        variant: "destructive"
      });
      return;
    }

    const reader = new FileReader();
    reader.onloadend = () => {
      setImage(reader.result as string);
    };
    reader.readAsDataURL(file);
  }, [checkAuth, hasRemainingQuota, setShowSubscriptionDialog, toast, t, setImage]);

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(true);
  }, []);

  const handleDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
  }, []);

  const handleDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);

    if (!checkAuth()) return;

    // 检查配额
    if (!hasRemainingQuota('image')) {
      setShowSubscriptionDialog(true);
      return;
    }

    const file = e.dataTransfer.files[0];
    if (file && file.type.startsWith('image/')) {
      const reader = new FileReader();
      reader.onloadend = () => {
        setImage(reader.result as string);
      };
      reader.readAsDataURL(file);
    } else {
      toast({
        title: t('error.invalidFile'),
        description: t('error.invalidFileDesc'),
        variant: "destructive"
      });
    }
  }, [checkAuth, hasRemainingQuota, setShowSubscriptionDialog, toast, t, setImage]);

  const handlePDFUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!checkAuth()) return;

    const file = e.target.files?.[0]
    if (!file || (!file.name.toLowerCase().endsWith('.pdf') && file.type !== 'application/pdf')) {
      toast({
        title: t('error.invalidFile'),
        description: t('error.invalidFileDesc'),
        variant: "destructive"
      });
      return;
    }

    // 保存PDF文件以便后续处理
    setPdfFile(file);
    
    // 创建PDF预览
    const reader = new FileReader();
    reader.onloadend = () => {
      setPdfPreview(reader.result as string);
    };
    reader.readAsDataURL(file);
    
    // 清空文件输入框,以便可以再次选择同一文件
    e.target.value = '';
  };

  const handleSpeechUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!checkAuth()) return;

    const file = e.target.files?.[0]
    if (!file || !file.type.startsWith('audio/')) {
      toast({
        title: t('error.invalidAudioFile'),
        description: t('error.invalidAudioFileDesc'),
        variant: "destructive"
      });
      return;
    }

    // 检查配额
    if (!hasRemainingQuota('speech')) {
      setShowSubscriptionDialog(true);
      return;
    }

    setIsProcessing(true);
    try {
      if (!asrServiceRef.current) {
        asrServiceRef.current = new TencentASRService();
      }

      const text = await asrServiceRef.current.recognizeAudio(
        file,
        (progress) => {
          setInterimText(progress);
        },
        (error) => {
          toast({
            title: t('error.audioRecognition'),
            description: error,
            variant: "destructive"
          });
        }
      );
      
      // 处理成功后才记录使用次数
      if (!await checkAndUpdateUsage('speech')) {
        setIsProcessing(false);
        setInterimText('');
        return;
      }
      
      setExtractedText(text);
      setInterimText('');
      toast({
        title: t('success.audioRecognized'),
        description: t('success.description')
      });
    } catch (error: any) {
      if (error.message !== '配额不足') {
        toast({
          title: t('error.audioProcessing'),
          description: String(error),
          variant: "destructive"
        });
      }
    } finally {
      setIsProcessing(false);
    }
  };

  const handleSpeechFile = async (file: File): Promise<string> => {
    if (!file.type.startsWith('audio/')) {
      throw new Error(t('error.invalidAudioFile'))
    }
    return await recognizeAudioFile(file)
  }

  const handleVideoUpload = async (file: File) => {
    if (!checkAuth()) return;

    if (!file || !file.type.startsWith('video/')) {
      toast({
        title: t('error.invalidVideoFile'),
        description: t('error.invalidVideoFileDesc'),
        variant: "destructive"
      });
      return;
    }

    // 检查配额
    if (!hasRemainingQuota('video')) {
      setShowSubscriptionDialog(true);
      return;
    }

    setVideoFile(file);
    setIsProcessing(true);
    try {
      if (videoService === 'zhipu') {
        console.log(t('console.videoFramesExtracting'));
        const frames = await extractVideoFrames(file);
        console.log(t('console.videoFramesExtracted', [frames.length]));
        console.log(t('console.videoFramesExample', [frames[0].substring(0, 100) + '...']));
        const text = await analyzeVideoContent(frames);
        console.log(t('console.videoProcessed', [text.length]));
        setExtractedText(text);
      } else if (videoService === 'aliyun') {
        try {
          // 直接上传到 OSS
          const videoUrl = await uploadToOSS(file);
          console.log(t('console.videoUploaded', [videoUrl]));

          // 创建视频识别任务
          const createResponse = await fetch('/api/aliyun/video-ocr/create', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              videoUrl: videoUrl,
            }),
          });

          if (!createResponse.ok) {
            const errorText = await createResponse.text();
            console.error(t('console.videoTaskCreateFailed', [errorText]));
            throw new Error(t('error.videoProcessingDesc'));
          }

          const createResult = await createResponse.json();
          if (!createResult.taskId) {
            throw new Error(t('error.videoProcessingDesc'));
          }

          console.log(t('console.videoTaskPolling'));
          const result = await pollTaskStatus(createResult.taskId);
          
          if (!result || !result.raw) {
            throw new Error(t('error.videoProcessingDesc'));
          }

          // 处理OCR结果
          console.log(t('console.videoOcrProcessing'));
          
          // 创建一个Map来存储时间戳对应的文本
          const textMap = new Map<number, Set<string>>();

          try {
            // 尝试解析 Result 字符串
            if (typeof result.raw === 'string') {
              result.raw = JSON.parse(result.raw);
            }

            // 处理 OCR 结果
            if (result.raw?.ocrResults?.length) {
              result.raw.ocrResults.forEach((item: any) => {
                if (item.detailInfo?.length) {
                  item.detailInfo.forEach((detail: any) => {
                    if (detail.text && detail.timeStamp) {
                      const timestamp = Math.floor(detail.timeStamp / 1000) * 1000;
                      const text = String(detail.text).trim();
                      if (text.length >= 2 && /[\u4e00-\u9fa5a-zA-Z0-9]/.test(text)) {
                        if (!textMap.has(timestamp)) {
                          textMap.set(timestamp, new Set());
                        }
                        textMap.get(timestamp)?.add(text);
                      }
                    }
                  });
                }
              });
            }

            // 处理视频 OCR 结果
            if (result.raw?.videoOcrResults?.length) {
              result.raw.videoOcrResults.forEach((item: any) => {
                if (item.detailInfo?.length) {
                  item.detailInfo.forEach((detail: any) => {
                    if (detail.text && detail.timeStamp) {
                      const timestamp = Math.floor(detail.timeStamp / 1000) * 1000;
                      const text = String(detail.text).trim();
                      if (text.length >= 2 && /[\u4e00-\u9fa5a-zA-Z0-9]/.test(text)) {
                        if (!textMap.has(timestamp)) {
                          textMap.set(timestamp, new Set());
                        }
                        textMap.get(timestamp)?.add(text);
                      }
                    }
                  });
                }
              });
            }

            // 处理字幕结果
            if (result.raw?.subtitlesResults?.[0]?.subtitlesChineseResults) {
              const subtitles = result.raw.subtitlesResults[0].subtitlesChineseResults;
              Object.entries(subtitles).forEach(([timeStr, text]: [string, any]) => {
                // 过滤掉时间轴格式的文本
                if (text && !timeStr.includes('-->') && !String(text).includes('-->')) {
                  const textStr = String(text).trim();
                  // 过滤掉 [object Object] 和其他无效内容
                  if (textStr.length >= 2 && 
                      /[\u4e00-\u9fa5a-zA-Z0-9]/.test(textStr) && 
                      !textStr.includes('[object Object]') &&
                      !textStr.match(/\d{2}:\d{2}:\d{2},\d{3}/)) {
                    // 对于字幕,我们使用一个固定的时间戳,因为我们只关心文本内容
                    const timestamp = 0;
                    if (!textMap.has(timestamp)) {
                      textMap.set(timestamp, new Set());
                    }
                    textMap.get(timestamp)?.add(textStr);
                  }
                }
              });
            }

            // 按时间戳排序并合并文本,去重
            const sortedTexts = Array.from(textMap.entries())
              .sort(([a], [b]) => a - b)
              .map(([_, texts]) => Array.from(texts).join(' '))
              .filter(text => text.length > 0);

            const combinedText = sortedTexts.join('\n');
            console.log(t('console.videoOcrResult', [combinedText]));
            
            if (combinedText) {
              setSourceText(combinedText);
              setExtractedText(combinedText);
            } else {
              console.log(t('console.videoNoText'));
              toast({
                title: t('error.noTextExtracted'),
                description: t('error.noTextExtractedDesc'),
                variant: "destructive"
              });
            }
          } catch (error) {
            console.error(t('console.videoOcrError'), error);
            // 如果解析失败,尝试直接使用原始结果
            if (result.raw && typeof result.raw === 'string') {
              setSourceText(result.raw);
              setExtractedText(result.raw);
            }
          }

          setIsProcessing(false);
          setVideoFile(null);
          
        } catch (uploadError: any) {
          console.error(t('console.videoUploadError'), uploadError);
          toast({
            title: t('error.videoUploadFailed'),
            description: uploadError.message || t('error.videoUploadFailedDesc'),
            variant: "destructive"
          });
          return;
        }
      } else {
        throw new Error(t('error.videoServiceNotSupported'));
      }

      // 处理成功后才记录使用次数
      if (!await checkAndUpdateUsage('video')) {
        setIsProcessing(false);
        setVideoFile(null);
        return;
      }

      toast({
        title: t('success.videoExtracted'),
        description: t('success.description')
      });
    } catch (error: any) {
      console.error(t('console.videoProcessingError'), error);
      toast({
        title: t('error.videoProcessing'),
        description: error.message || t('error.videoProcessingDesc'),
        variant: "destructive"
      });
    } finally {
      setIsProcessing(false);
      setVideoFile(null);
    }
  };

  const handleVideoFile = async (file: File): Promise<string> => {
    if (!file.type.startsWith('video/')) {
      throw new Error(t('error.invalidVideoFile'))
    }
    return await processVideoFile(file)
  }

  // 处理文件变更
  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    // 获取当前标签页类型
    const currentTab = activeTab.toLowerCase()
    
    // 检查配额
    if (currentTab !== 'text' && !hasRemainingQuota(currentTab as keyof Omit<QuotaInfo['usage'], 'text'>)) {
      setShowSubscriptionDialog(true);
      return;
    }

    // 根据不同类型处理文件
    try {
      switch (currentTab) {
        case 'image':
          if (!file.type.startsWith('image/')) {
            throw new Error(t('error.invalidImageFile'))
          }
          await handleImageUpload(e)
          break
        
        case 'file':
          if (!file.name.toLowerCase().endsWith('.pdf') && file.type !== 'application/pdf') {
            throw new Error(t('error.invalidPDFFile'))
          }
          await handlePDFUpload(e)
          break
        
        case 'speech':
          if (!file.type.startsWith('audio/')) {
            throw new Error(t('error.invalidAudioFile'))
          }
          // 检查配额
          if (!hasRemainingQuota('speech')) {
            setShowSubscriptionDialog(true);
            return;
          }
          // 记录使用次数
          if (!await checkAndUpdateUsage('speech')) {
            return;
          }
          await handleSpeechUpload(e)
          break
        
        case 'video':
          if (!file.type.startsWith('video/')) {
            throw new Error(t('error.invalidVideoFile'))
          }
          await handleVideoUpload(file)
          break
      }
    } catch (error: any) {
      console.error(t('console.fileProcessingError'), error)
      if (error.message !== '配额不足') {
        toast({
          variant: "destructive",
          title: t('error'),
          description: error.message || t('uploadFailed')
        })
      }
    } finally {
      // 清空文件输入框
      e.target.value = ''
    }
  }

  // 处理图片文件
  const handleImageFile = async (file: File): Promise<string> => {
    if (!file.type.startsWith('image/')) {
      throw new Error(t('error.invalidImageFile'))
    }
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onloadend = () => resolve(reader.result as string)
      reader.onerror = reject
      reader.readAsDataURL(file)
    })
  }

  // 处理PDF文件
  const handlePDFFile = async (file: File): Promise<string> => {
    if (!file.type.endsWith('pdf') && !file.type.startsWith('application/pdf')) {
      throw new Error(t('error.invalidPDFFile'))
    }
    return await extractPDFWithKimi(file)
  }

  // 处理语音文件
  const recognizeAudioFile = async (file: File): Promise<string> => {
    // 检查配额
    if (!hasRemainingQuota('speech')) {
      setShowSubscriptionDialog(true);
      throw new Error('配额不足');
    }

    // 记录使用次数
    if (!await checkAndUpdateUsage('speech')) {
      throw new Error('配额不足');
    }

    if (!asrServiceRef.current) {
      asrServiceRef.current = new TencentASRService();
    }

    return await asrServiceRef.current.recognizeAudio(
      file,
      (progress) => {
        setInterimText(progress);
      },
      (error) => {
        toast({
          title: t('error.audioRecognition'),
          description: error,
          variant: "destructive"
        });
      }
    );
  };

  // 处理视频文件
  const processVideoFile = async (file: File): Promise<string> => {
    if (videoService === 'zhipu') {
      const frames = await extractVideoFrames(file);
      return await analyzeVideoContent(frames);
    } else if (videoService === 'aliyun') {
      const videoUrl = await uploadToOSS(file);
      const createResponse = await fetch('/api/aliyun/video-ocr/create', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ videoUrl }),
      });

      if (!createResponse.ok) {
        throw new Error(t('error.videoProcessingDesc'));
      }

      const createResult = await createResponse.json();
      if (!createResult.taskId) {
        throw new Error(t('error.videoProcessingDesc'));
      }

      const result = await pollTaskStatus(createResult.taskId);
      if (!result || !result.raw) {
        throw new Error(t('error.videoProcessingDesc'));
      }

      return result.raw;
    }
    
    throw new Error(t('error.videoServiceNotSupported'));
  };

  // 处理文本翻译
  const handleTextTranslate = async () => {
    if (!sourceText) {
      toast({
        title: t('error.noText'),
        description: t('error.noTextDesc'),
        variant: "destructive"
      });
      return;
    }

    if (!selectedLanguage) {
      toast({
        title: t('error.noLanguage'),
        description: t('error.noLanguageDesc'),
        variant: "destructive"
      });
      return;
    }

    setIsProcessing(true);
    try {
      let result: string;
      switch (translationService) {
        case 'deepseek':
          result = await translateWithDeepSeek(sourceText, selectedLanguage)
          break;
        case 'qwen':
          result = await translateWithQwen(sourceText, selectedLanguage)
          break;
        case 'gemini':
          result = await translateText(sourceText, selectedLanguage)
          break;
        case 'zhipu':
          result = await translateWithZhipu(sourceText, selectedLanguage)
          break;
        case 'hunyuan':
          result = await translateWithHunyuan(sourceText, selectedLanguage)
          break;
        case 'step':
          result = await translateWithStepAPI(sourceText, selectedLanguage)
          break;
        default:
          result = await translateWithDeepSeek(sourceText, selectedLanguage)
      }
      setTranslatedText(result);
      toast({
        title: t('success.translated'),
        description: t('success.description')
      });
    } catch (error: any) {
      toast({
        title: t('errors.translationError'),
        description: error.message || t('errors.translationDesc'),
        variant: "destructive"
      });
    } finally {
      setIsProcessing(false);
    }
  };

  // 处理文本优化
  const handleImprove = async () => {
    if (!translatedText) {
      toast({
        title: t('error.noTranslation'),
        description: t('error.noTranslationDesc'),
        variant: "destructive"
      });
      return;
    }

    setIsProcessing(true);
    try {
      const improved = await improveText(translatedText, selectedLanguage);
      setTranslatedText(improved);
      toast({
        title: t('success.improved'),
        description: t('success.description')
      });
    } catch (error: any) {
      toast({
        title: t('error.improving'),
        description: error.message || t('error.improvingDesc'),
        variant: "destructive"
      });
    } finally {
      setIsProcessing(false);
    }
  };

  // 处理语音识别开关
  const toggleSpeechRecognition = async () => {
    if (!checkAuth()) return;

    // 检查配额
    if (!isListening && !hasRemainingQuota('speech')) {
      setShowSubscriptionDialog(true);
      return;
    }

    if (!asrServiceRef.current) {
      toast({
        title: t('error.speechNotSupported'),
        description: t('error.speechNotSupportedDesc'),
        variant: "destructive"
      });
      return;
    }

    if (isListening) {
      if (recognition.current) {
        recognition.current.stop();
        recognition.current = null;
      }
      setIsListening(false);
      setInterimText('');
    } else {
      // 记录使用次数
      if (!await checkAndUpdateUsage('speech')) {
        return;
      }

      const rec = await asrServiceRef.current.recognizeStream(
        (text, isFinal) => {
          if (isFinal) {
            setExtractedText(text);
            setInterimText('');
            toast({
              title: t('success.speechRecognized'),
              description: t('success.description')
            });
          } else {
            setInterimText(text);
          }
        },
        (error) => {
          toast({
            title: t('error.speechRecognition'),
            description: error,
            variant: "destructive"
          });
          setIsListening(false);
        }
      );

      if (rec) {
        recognition.current = rec;
        rec.start();
        setIsListening(true);
      }
    }
  };

  // 处理图片文本提取
  const handleExtractText = async () => {
    if (!image) {
      toast({
        title: t('error.noImage'),
        description: t('error.noImageDesc'),
        variant: "destructive"
      });
      return;
    }

    // 检查配额
    if (!hasRemainingQuota('image')) {
      setShowSubscriptionDialog(true);
      return;
    }

    setIsProcessing(true);
    try {
      let result: string;
      switch (ocrService) {
        case 'tencent':
          result = await extractTextWithTencent(image);
          break;
        case 'qwen':
          result = await extractTextWithQwen(image);
          break;
        case 'gemini':
          result = await extractTextFromImage(image);
          break;
        case 'zhipu':
          result = await extractTextWithZhipu(image);
          break;
        case 'kimi':
          result = await extractTextWithKimi(image);
          break;
        case 'step':
          result = await extractTextWithStep(image);
          break;
        default:
          result = await extractTextWithQwen(image);
      }

      // 处理成功后才记录使用次数
      if (!await checkAndUpdateUsage('image')) {
        setIsProcessing(false);
        return;
      }

      setExtractedText(result);
      toast({
        title: t('success.extracted'),
        description: t('success.description')
      });
    } catch (error: any) {
      toast({
        title: t('errors.extract.extractingError'),
        description: error.message || t('errors.extract.extractingDesc'),
        variant: "destructive"
      });
    } finally {
      setIsProcessing(false);
    }
  };

  // 处理翻译
  const handleTranslate = async () => {
    if (!extractedText && !fileContent || !selectedLanguage) {
      toast({
        title: t('error.translating'),
        description: t('error.noLanguage'),
        variant: "destructive"
      });
      return;
    }

    setIsProcessing(true);
    try {
      let result: string;
      try {
        switch (translationService) {
          case 'deepseek':
            result = await translateWithDeepSeek(extractedText || fileContent, selectedLanguage);
            break;
          case 'qwen':
            result = await translateWithQwen(extractedText || fileContent, selectedLanguage);
            break;
          case 'gemini':
            result = await translateText(extractedText || fileContent, selectedLanguage);
            break;
          case 'zhipu':
            result = await translateWithZhipu(extractedText || fileContent, selectedLanguage);
            break;
          case 'hunyuan':
            result = await translateWithHunyuan(extractedText || fileContent, selectedLanguage);
            break;
          case 'step':
            result = await translateWithStepAPI(extractedText || fileContent, selectedLanguage);
            break;
          default:
            result = await translateWithDeepSeek(extractedText || fileContent, selectedLanguage);
        }
      } catch (serviceError: any) {
        console.error(`${translationService} ${t('console.translationServiceError')}:`, serviceError);
        if (translationService !== 'deepseek') {
          console.log(t('console.tryingDeepSeek'));
          result = await translateWithDeepSeek(extractedText || fileContent, selectedLanguage);
        } else {
          throw serviceError;
        }
      }

      setTranslatedText(result);
      toast({
        title: t('success.translated'),
        description: t('success.description')
      });
    } catch (error: any) {
      console.error(t('console.translationError'), error);
      toast({
        title: t('errors.translationError'),
        description: error.message || t('errors.translationDesc'),
        variant: "destructive"
      });
    } finally {
      setIsProcessing(false);
    }
  };

  const handleExtractPDFText = async () => {
    if (!pdfFile) {
      toast({
        title: t('error.invalidFile'),
        description: t('error.invalidFileDesc'),
        variant: "destructive"
      });
      return;
    }

    // 检查配额
    if (!hasRemainingQuota('pdf')) {
      setShowSubscriptionDialog(true);
      return;
    }

    try {
      setIsFileProcessing(true);
      
      // 使用通用PDF处理函数,传入选择的服务商
      console.log(t('console.pdfProcessing', [fileService]));
      const content = await extractPDFContent(pdfFile, fileService as 'kimi' | 'mistral', (status) => {
        console.log(t('console.pdfStatus', [status]));
        toast({
          title: status,
          description: t('success.description')
        });
      });
      
      console.log(t('console.pdfProcessed', [content?.length || 0]));
      console.log(t('console.pdfPreview', [content?.substring(0, 100)]));
      
      // 处理成功后才记录使用次数
      if (!await checkAndUpdateUsage('pdf')) {
        setIsFileProcessing(false);
        return;
      }
      
      // 将提取的内容设置到提取文本区域
      setExtractedText(content);
      // 同时设置到sourceText,确保翻译时能使用
      setSourceText(content);
      
      toast({
        title: t('success.fileExtracted'),
        description: t('success.description')
      });
    } catch (error: any) {
      console.error(t('console.fileProcessingError'), error);
      toast({
        title: t('error.fileProcessing'),
        description: error.message || t('error.fileProcessingDesc'),
        variant: "destructive"
      });
    } finally {
      setIsFileProcessing(false);
    }
  };

  useEffect(() => {
    setMounted(true)
  }, [])

  // 在切换标签页时清空状态
  const handleTabChange = (value: string) => {
    setActiveTab(value);
    setExtractedText('');
    setTranslatedText('');
    setInterimText('');
    setSourceText('');
    setSelectedLanguage('');
    if (value !== 'image') {
      setImage(null);
    }
    if (value !== 'video') {
      setVideoFile(null);
    }
    if (value !== 'file') {
      setFileContent('');
    }
  };

  // 轮询任务状态
  const pollTaskStatus = async (taskId: string) => {
    let attempts = 0;
    const POLL_INTERVAL = 5000; // 5秒
    const MAX_POLL_ATTEMPTS = 60; // 最多轮询60次,即5分钟
    
    while (attempts < MAX_POLL_ATTEMPTS) {
      try {
        const response = await fetch('/api/aliyun/video-ocr/status', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ taskId }),
        });

        const result = await response.json();
        console.log(t('console.videoTaskResult', [result]));
        
        if (result.status === 'success') {
          return result.data;
        }
        
        if (!result.success) {
          throw new Error(result.message || t('console.videoTaskFailed'));
        }
        
        await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
        attempts++;
        
      } catch (error) {
        console.error(t('console.videoTaskQueryFailed'), error);
        throw error;
      }
    }
    
    throw new Error(t('console.videoTaskTimeout'));
  };

  return (
    <div className="container mx-auto px-4 py-8">
      {!mounted ? null : (
        <>
          <Card className="p-4 md:p-6">
            <Tabs defaultValue="text" className="w-full" onValueChange={handleTabChange}>
              <TabsList className="grid w-full grid-cols-2 sm:grid-cols-5 gap-2 h-auto mb-6">
                <TabsTrigger value="text" className="data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2">
                  <Languages className="w-4 h-4 mr-1 sm:mr-2" />
                  <span className="text-xs sm:text-sm">{t('tabs.text')}</span>
                </TabsTrigger>
                <TabsTrigger value="image" className="data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2">
                  <ImageIcon className="w-4 h-4 mr-1 sm:mr-2" />
                  <span className="text-xs sm:text-sm">{t('tabs.image')}</span>
                </TabsTrigger>
                <TabsTrigger value="file" className="data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2">
                  <FileType className="w-4 h-4 mr-1 sm:mr-2" />
                  <span className="text-xs sm:text-sm">{t('tabs.pdf')}</span>
                </TabsTrigger>
                <TabsTrigger value="speech" className="data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2">
                  {isListening ? <MicOff className="w-4 h-4 mr-1 sm:mr-2" /> : <Mic className="w-4 h-4 mr-1 sm:mr-2" />}
                  <span className="text-xs sm:text-sm">{t('tabs.speech')}</span>
                </TabsTrigger>
                <TabsTrigger value="video" className="data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2">
                  <Video className="w-4 h-4 mr-1 sm:mr-2" />
                  <span className="text-xs sm:text-sm">{t('tabs.video')}</span>
                </TabsTrigger>
              </TabsList>

              <TabsContent value="text">
                <div className="flex flex-col items-center justify-center gap-4">
                  <textarea
                    value={sourceText}
                    onChange={(e) => setSourceText(e.target.value)}
                    placeholder={t('enterText')}
                    className="w-full h-32 sm:h-40 p-4 rounded-lg border border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-primary text-sm sm:text-base"
                  />

                  <div className="flex flex-col w-full gap-3">
                    <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
                      <Select 
                        onValueChange={(value) => {
                          console.log(t('console.selectedLanguage', [value]))
                          setSelectedL
Download .txt
gitextract_k73jqg37/

├── .eslintrc.json
├── .gitignore
├── README.md
├── app/
│   ├── api/
│   │   ├── aliyun/
│   │   │   ├── oss/
│   │   │   │   ├── sts/
│   │   │   │   │   └── route.ts
│   │   │   │   └── upload/
│   │   │   │       └── route.ts
│   │   │   └── video-ocr/
│   │   │       ├── create/
│   │   │       │   └── route.ts
│   │   │       ├── query/
│   │   │       │   └── route.ts
│   │   │       └── status/
│   │   │           └── route.ts
│   │   ├── asr/
│   │   │   ├── aliyun/
│   │   │   │   └── recognize/
│   │   │   │       └── route.ts
│   │   │   ├── create/
│   │   │   │   └── route.ts
│   │   │   └── status/
│   │   │       └── route.ts
│   │   ├── auth/
│   │   │   ├── [...nextauth]/
│   │   │   │   ├── auth.ts
│   │   │   │   └── route.ts
│   │   │   └── register/
│   │   │       └── route.ts
│   │   ├── file/
│   │   │   └── extract/
│   │   │       └── route.ts
│   │   ├── ocr/
│   │   │   ├── kimi/
│   │   │   │   └── route.ts
│   │   │   ├── route.ts
│   │   │   └── step/
│   │   │       └── route.ts
│   │   ├── qwen/
│   │   │   ├── ocr/
│   │   │   │   └── route.ts
│   │   │   └── translate/
│   │   │       └── route.ts
│   │   ├── register/
│   │   │   └── route.ts
│   │   ├── subscription/
│   │   │   └── route.ts
│   │   ├── tencent/
│   │   │   └── ocr/
│   │   │       └── route.ts
│   │   ├── translate/
│   │   │   ├── claude/
│   │   │   │   └── route.ts
│   │   │   ├── kimi/
│   │   │   │   └── route.ts
│   │   │   ├── route.ts
│   │   │   ├── siliconflow/
│   │   │   │   └── route.ts
│   │   │   └── step/
│   │   │       └── route.ts
│   │   ├── upload/
│   │   │   └── route.ts
│   │   ├── user/
│   │   │   ├── info/
│   │   │   │   └── route.ts
│   │   │   ├── update/
│   │   │   │   └── route.ts
│   │   │   └── usage/
│   │   │       └── route.ts
│   │   └── webhook/
│   │       └── route.ts
│   ├── globals.css
│   ├── layout.tsx
│   ├── login/
│   │   ├── error.tsx
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── page.tsx
│   ├── pricing/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── profile/
│   │   └── page.tsx
│   ├── providers.tsx
│   ├── register/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── translate/
│       ├── layout.tsx
│       └── page.tsx
├── components/
│   ├── client-layout.tsx
│   ├── footer.tsx
│   ├── google-analytics.tsx
│   ├── header.tsx
│   ├── language-provider.tsx
│   ├── language-switcher.tsx
│   ├── language-toggle.tsx
│   ├── layout.tsx
│   ├── subscription-dialog.tsx
│   ├── testimonials.tsx
│   ├── theme-provider.tsx
│   ├── theme-toggle.tsx
│   └── ui/
│       ├── accordion.tsx
│       ├── alert-dialog.tsx
│       ├── alert.tsx
│       ├── aspect-ratio.tsx
│       ├── avatar.tsx
│       ├── badge.tsx
│       ├── breadcrumb.tsx
│       ├── button.tsx
│       ├── calendar.tsx
│       ├── card.tsx
│       ├── carousel.tsx
│       ├── chart.tsx
│       ├── checkbox.tsx
│       ├── collapsible.tsx
│       ├── command.tsx
│       ├── context-menu.tsx
│       ├── dialog.tsx
│       ├── drawer.tsx
│       ├── dropdown-menu.tsx
│       ├── form.tsx
│       ├── hover-card.tsx
│       ├── input-otp.tsx
│       ├── input.tsx
│       ├── label.tsx
│       ├── menubar.tsx
│       ├── navigation-menu.tsx
│       ├── pagination.tsx
│       ├── popover.tsx
│       ├── progress.tsx
│       ├── radio-group.tsx
│       ├── resizable.tsx
│       ├── scroll-area.tsx
│       ├── select.tsx
│       ├── separator.tsx
│       ├── sheet.tsx
│       ├── skeleton.tsx
│       ├── slider.tsx
│       ├── sonner.tsx
│       ├── switch.tsx
│       ├── table.tsx
│       ├── tabs.tsx
│       ├── textarea.tsx
│       ├── toast.tsx
│       ├── toaster.tsx
│       ├── toggle-group.tsx
│       ├── toggle.tsx
│       ├── tooltip.tsx
│       └── use-toast.ts
├── components.json
├── hooks/
│   └── use-toast.ts
├── lib/
│   ├── aliyun-oss-client.ts
│   ├── aliyun-oss-upload.ts
│   ├── aliyun-oss.ts
│   ├── aliyun-video-ocr.ts
│   ├── db/
│   │   └── migrate.ts
│   ├── deepseek.ts
│   ├── gemini.ts
│   ├── hooks/
│   │   ├── use-analytics.ts
│   │   └── use-quota.ts
│   ├── i18n/
│   │   ├── locales/
│   │   │   ├── en.json
│   │   │   └── zh.json
│   │   ├── translations.ts
│   │   └── use-translations.ts
│   ├── kimi.ts
│   ├── languages.ts
│   ├── qwen.ts
│   ├── server/
│   │   ├── tencent-sign.ts
│   │   └── translate.ts
│   ├── speech.ts
│   ├── step.ts
│   ├── stripe.ts
│   ├── tencent-asr.ts
│   ├── tencent-sign.ts
│   ├── tencent.ts
│   ├── utils.ts
│   └── zhipu.ts
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public/
│   ├── ads.txt
│   └── site.webmanifest
├── tailwind.config.js
├── tailwind.config.ts
├── tsconfig.json
└── types/
    ├── alicloud.d.ts
    └── next-auth.d.ts
Download .txt
SYMBOL INDEX (255 symbols across 94 files)

FILE: app/api/aliyun/oss/sts/route.ts
  function GET (line 5) | async function GET() {

FILE: app/api/aliyun/oss/upload/route.ts
  function POST (line 5) | async function POST(request: Request) {

FILE: app/api/aliyun/video-ocr/create/route.ts
  type AsyncJobResult (line 4) | interface AsyncJobResult {
  function POST (line 9) | async function POST(request: Request) {

FILE: app/api/aliyun/video-ocr/query/route.ts
  type AsyncJobQueryResult (line 4) | interface AsyncJobQueryResult {
  function POST (line 13) | async function POST(request: Request) {

FILE: app/api/aliyun/video-ocr/status/route.ts
  type VideoOCRResult (line 4) | interface VideoOCRResult {
  type AsyncJobQueryResult (line 30) | interface AsyncJobQueryResult {
  function POST (line 39) | async function POST(request: Request) {

FILE: app/api/asr/aliyun/recognize/route.ts
  function POST (line 3) | async function POST(request: Request) {

FILE: app/api/asr/create/route.ts
  function POST (line 10) | async function POST(request: Request) {

FILE: app/api/asr/status/route.ts
  function POST (line 10) | async function POST(request: Request) {

FILE: app/api/auth/[...nextauth]/auth.ts
  method authorize (line 23) | async authorize(credentials) {
  method profile (line 60) | profile(profile) {
  method profile (line 75) | profile(profile) {
  method signIn (line 90) | async signIn({ user, account }) {
  method jwt (line 163) | async jwt({ token, user }) {
  method session (line 169) | async session({ session }) {

FILE: app/api/auth/register/route.ts
  function POST (line 5) | async function POST(req: Request) {

FILE: app/api/file/extract/route.ts
  constant KIMI_API_KEY (line 4) | const KIMI_API_KEY = process.env.NEXT_PUBLIC_KIMI_API_KEY
  constant KIMI_API_URL (line 5) | const KIMI_API_URL = 'https://api.moonshot.cn/v1'
  constant MISTRAL_API_KEY (line 6) | const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY
  constant TIMEOUT (line 9) | const TIMEOUT = {
  function fetchWithTimeout (line 16) | async function fetchWithTimeout(url: string, options: RequestInit, timeo...
  function uploadFile (line 44) | async function uploadFile(file: string, filename: string) {
  function getFileContent (line 76) | async function getFileContent(fileId: string) {
  function processContent (line 100) | async function processContent(content: string) {
  function processPdfWithMistral (line 152) | async function processPdfWithMistral(file: string, filename: string) {
  function POST (line 333) | async function POST(request: Request) {

FILE: app/api/ocr/kimi/route.ts
  constant KIMI_API_KEY (line 4) | const KIMI_API_KEY = process.env.NEXT_PUBLIC_KIMI_API_KEY
  constant KIMI_API_URL (line 5) | const KIMI_API_URL = 'https://api.moonshot.cn/v1'
  function POST (line 7) | async function POST(request: Request) {

FILE: app/api/ocr/route.ts
  type TextDetection (line 6) | interface TextDetection {
  type OCRResponse (line 16) | interface OCRResponse {
  function POST (line 38) | async function POST(request: Request) {

FILE: app/api/ocr/step/route.ts
  function POST (line 7) | async function POST(request: Request) {

FILE: app/api/qwen/ocr/route.ts
  function POST (line 7) | async function POST(request: Request) {

FILE: app/api/qwen/translate/route.ts
  function POST (line 5) | async function POST(request: Request) {

FILE: app/api/register/route.ts
  function POST (line 7) | async function POST(req: Request) {

FILE: app/api/subscription/route.ts
  function POST (line 6) | async function POST(req: Request) {

FILE: app/api/tencent/ocr/route.ts
  function POST (line 7) | async function POST(request: Request) {

FILE: app/api/translate/claude/route.ts
  function POST (line 4) | async function POST(request: Request) {

FILE: app/api/translate/kimi/route.ts
  function POST (line 4) | async function POST(request: Request) {

FILE: app/api/translate/route.ts
  function POST (line 5) | async function POST(request: Request) {
  function translateWithDeepSeekAPI (line 76) | async function translateWithDeepSeekAPI(text: string, targetLanguage: st...
  function translateWithQwenAPI (line 106) | async function translateWithQwenAPI(text: string, targetLanguage: string) {
  function translateWithZhipuAPI (line 136) | async function translateWithZhipuAPI(text: string, targetLanguage: strin...
  function translateWith4oMiniAPI (line 166) | async function translateWith4oMiniAPI(text: string, targetLanguage: stri...
  function translateWithHunyuanAPI (line 196) | async function translateWithHunyuanAPI(text: string, targetLanguage: str...
  function translateWithMinniMaxAPI (line 228) | async function translateWithMinniMaxAPI(text: string, targetLanguage: st...
  function translateWithSiliconFlowAPI (line 263) | async function translateWithSiliconFlowAPI(text: string, targetLanguage:...
  function translateWithClaudeAPI (line 298) | async function translateWithClaudeAPI(text: string, targetLanguage: stri...
  function translateWithStepAPI (line 328) | async function translateWithStepAPI(text: string, targetLanguage: string) {

FILE: app/api/translate/siliconflow/route.ts
  function POST (line 4) | async function POST(request: Request) {

FILE: app/api/translate/step/route.ts
  function POST (line 4) | async function POST(req: Request) {

FILE: app/api/upload/route.ts
  function POST (line 4) | async function POST(request: Request) {

FILE: app/api/user/info/route.ts
  type User (line 6) | interface User {
  type UsageType (line 26) | type UsageType = 'text' | 'image' | 'pdf' | 'speech' | 'video';
  type UsageRecord (line 28) | interface UsageRecord {
  type UsageInfo (line 33) | interface UsageInfo {
  function GET (line 42) | async function GET() {

FILE: app/api/user/update/route.ts
  function PUT (line 6) | async function PUT(req: Request) {

FILE: app/api/user/usage/route.ts
  function checkQuota (line 9) | async function checkQuota(userId: number, type: string) {
  function getUsageCount (line 54) | async function getUsageCount(userId: number, type: string) {
  function recordUsage (line 66) | async function recordUsage(userId: number, type: string) {
  function POST (line 141) | async function POST(req: Request) {

FILE: app/api/webhook/route.ts
  function POST (line 8) | async function POST(req: Request) {

FILE: app/layout.tsx
  function RootLayout (line 68) | function RootLayout({

FILE: app/login/error.tsx
  function ErrorPage (line 8) | function ErrorPage() {

FILE: app/login/layout.tsx
  function LoginLayout (line 8) | function LoginLayout({

FILE: app/login/page.tsx
  function LoginPage (line 15) | function LoginPage() {

FILE: app/page.tsx
  function Home (line 31) | function Home() {

FILE: app/pricing/layout.tsx
  function PricingLayout (line 24) | function PricingLayout({

FILE: app/pricing/page.tsx
  function PricingPage (line 14) | function PricingPage() {

FILE: app/profile/page.tsx
  type UserInfo (line 18) | interface UserInfo {
  constant QUOTA_TYPES (line 48) | const QUOTA_TYPES = ['text', 'image', 'pdf', 'speech', 'video'] as const
  function ProfilePage (line 50) | function ProfilePage() {

FILE: app/providers.tsx
  function Providers (line 10) | function Providers({ children }: { children: React.ReactNode }) {

FILE: app/register/layout.tsx
  function RegisterLayout (line 8) | function RegisterLayout({

FILE: app/register/page.tsx
  function RegisterPage (line 15) | function RegisterPage() {

FILE: app/translate/layout.tsx
  function TranslateLayout (line 24) | function TranslateLayout({

FILE: app/translate/page.tsx
  type QuotaInfo (line 47) | interface QuotaInfo {
  function TranslatePage (line 62) | function TranslatePage() {

FILE: components/client-layout.tsx
  function ClientLayout (line 7) | function ClientLayout({

FILE: components/footer.tsx
  function Footer (line 7) | function Footer() {

FILE: components/header.tsx
  function Header (line 23) | function Header() {

FILE: components/language-provider.tsx
  type Translations (line 6) | type Translations = {
  type LanguageContextType (line 25) | type LanguageContextType = {
  function LanguageProvider (line 33) | function LanguageProvider({ children }: { children: React.ReactNode }) {
  function useLanguage (line 67) | function useLanguage() {

FILE: components/language-switcher.tsx
  function LanguageSwitcher (line 13) | function LanguageSwitcher() {

FILE: components/language-toggle.tsx
  function LanguageToggle (line 11) | function LanguageToggle() {

FILE: components/layout.tsx
  function Layout (line 6) | function Layout({ children }: { children: React.ReactNode }) {

FILE: components/subscription-dialog.tsx
  type SubscriptionDialogProps (line 6) | interface SubscriptionDialogProps {
  function SubscriptionDialog (line 11) | function SubscriptionDialog({

FILE: components/testimonials.tsx
  function Testimonials (line 16) | function Testimonials() {

FILE: components/theme-provider.tsx
  function ThemeProvider (line 7) | function ThemeProvider({ children, ...props }: ThemeProviderProps) {

FILE: components/theme-toggle.tsx
  function ThemeToggle (line 12) | function ThemeToggle() {

FILE: components/ui/badge.tsx
  type BadgeProps (line 26) | interface BadgeProps
  function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: components/ui/button.tsx
  type ButtonProps (line 36) | interface ButtonProps

FILE: components/ui/calendar.tsx
  type CalendarProps (line 10) | type CalendarProps = React.ComponentProps<typeof DayPicker>;
  function Calendar (line 12) | function Calendar({

FILE: components/ui/carousel.tsx
  type CarouselApi (line 12) | type CarouselApi = UseEmblaCarouselType[1];
  type UseCarouselParameters (line 13) | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
  type CarouselOptions (line 14) | type CarouselOptions = UseCarouselParameters[0];
  type CarouselPlugin (line 15) | type CarouselPlugin = UseCarouselParameters[1];
  type CarouselProps (line 17) | type CarouselProps = {
  type CarouselContextProps (line 24) | type CarouselContextProps = {
  function useCarousel (line 35) | function useCarousel() {

FILE: components/ui/chart.tsx
  constant THEMES (line 9) | const THEMES = { light: '', dark: '.dark' } as const;
  type ChartConfig (line 11) | type ChartConfig = {
  type ChartContextProps (line 21) | type ChartContextProps = {
  function useChart (line 27) | function useChart() {
  function getPayloadConfigFromPayload (line 320) | function getPayloadConfigFromPayload(

FILE: components/ui/command.tsx
  type CommandDialogProps (line 26) | interface CommandDialogProps extends DialogProps {}

FILE: components/ui/form.tsx
  type FormFieldContextValue (line 20) | type FormFieldContextValue<
  type FormItemContextValue (line 67) | type FormItemContextValue = {

FILE: components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: components/ui/pagination.tsx
  type PaginationLinkProps (line 37) | type PaginationLinkProps = {

FILE: components/ui/sheet.tsx
  type SheetContentProps (line 52) | interface SheetContentProps

FILE: components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: components/ui/sonner.tsx
  type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>;

FILE: components/ui/textarea.tsx
  type TextareaProps (line 5) | interface TextareaProps

FILE: components/ui/toast.tsx
  type ToastProps (line 115) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
  type ToastActionElement (line 117) | type ToastActionElement = React.ReactElement<typeof ToastAction>

FILE: components/ui/toaster.tsx
  function Toaster (line 13) | function Toaster() {

FILE: components/ui/use-toast.ts
  constant TOAST_LIMIT (line 10) | const TOAST_LIMIT = 1
  constant TOAST_REMOVE_DELAY (line 11) | const TOAST_REMOVE_DELAY = 1000000
  type ToasterToast (line 13) | type ToasterToast = ToastProps & {
  function genId (line 29) | function genId() {
  type ActionType (line 34) | type ActionType = typeof actionTypes
  type Action (line 36) | type Action =
  type State (line 54) | interface State {
  function dispatch (line 133) | function dispatch(action: Action) {
  type Toast (line 140) | type Toast = Omit<ToasterToast, "id">
  function toast (line 142) | function toast({ ...props }: Toast) {
  function useToast (line 171) | function useToast() {

FILE: hooks/use-toast.ts
  constant TOAST_LIMIT (line 8) | const TOAST_LIMIT = 1;
  constant TOAST_REMOVE_DELAY (line 9) | const TOAST_REMOVE_DELAY = 1000000;
  type ToasterToast (line 11) | type ToasterToast = ToastProps & {
  function genId (line 27) | function genId() {
  type ActionType (line 32) | type ActionType = typeof actionTypes;
  type Action (line 34) | type Action =
  type State (line 52) | interface State {
  function dispatch (line 133) | function dispatch(action: Action) {
  type Toast (line 140) | type Toast = Omit<ToasterToast, 'id'>;
  function toast (line 142) | function toast({ ...props }: Toast) {
  function useToast (line 171) | function useToast() {

FILE: lib/aliyun-oss-client.ts
  type STSToken (line 3) | interface STSToken {
  function getSTSToken (line 14) | async function getSTSToken(): Promise<STSToken> {
  function uploadToOSS (line 26) | async function uploadToOSS(file: File): Promise<string> {

FILE: lib/aliyun-oss-upload.ts
  type STSToken (line 3) | interface STSToken {
  function getSTSToken (line 14) | async function getSTSToken(): Promise<STSToken> {
  function uploadToOSS (line 26) | async function uploadToOSS(file: File): Promise<string> {

FILE: lib/aliyun-oss.ts
  function uploadToOSS (line 3) | async function uploadToOSS(file: File): Promise<string> {

FILE: lib/aliyun-video-ocr.ts
  type VideoOCRResponse (line 3) | interface VideoOCRResponse {
  function extractVideoTextWithAliyun (line 14) | async function extractVideoTextWithAliyun(videoUrl: string): Promise<str...

FILE: lib/db/migrate.ts
  function migrate (line 12) | async function migrate() {

FILE: lib/deepseek.ts
  constant API_URL (line 3) | const API_URL = 'https://api.deepseek.com/v1/chat/completions'
  function retryWithDelay (line 6) | async function retryWithDelay<T>(
  function translateWithDeepSeek (line 25) | async function translateWithDeepSeek(text: string, targetLang: string) {
  function extractTextWithDeepseek (line 79) | async function extractTextWithDeepseek(file: File): Promise<string> {

FILE: lib/gemini.ts
  function retryWithDelay (line 8) | async function retryWithDelay<T>(
  function extractTextFromImage (line 27) | async function extractTextFromImage(imageData: string) {
  function translateText (line 62) | async function translateText(text: string, targetLang: string) {
  function improveText (line 103) | async function improveText(text: string, targetLang: string) {

FILE: lib/hooks/use-quota.ts
  type UsageType (line 4) | type UsageType = 'text' | 'image' | 'pdf' | 'speech' | 'video'
  type UseQuotaOptions (line 6) | interface UseQuotaOptions {
  function useQuota (line 11) | function useQuota(options: UseQuotaOptions = {}) {

FILE: lib/i18n/use-translations.ts
  type I18nStore (line 7) | type I18nStore = {

FILE: lib/kimi.ts
  function extractTextWithKimi (line 4) | async function extractTextWithKimi(image: string): Promise<string> {
  function retryWithDelay (line 43) | async function retryWithDelay<T>(
  function extractPDFContent (line 66) | async function extractPDFContent(
  function extractPDFWithKimi (line 180) | async function extractPDFWithKimi(

FILE: lib/languages.ts
  type Language (line 1) | interface Language {

FILE: lib/qwen.ts
  function retryWithDelay (line 4) | async function retryWithDelay<T>(
  function translateWithQwen (line 23) | async function translateWithQwen(text: string, targetLang: string) {
  function extractTextWithQwen (line 64) | async function extractTextWithQwen(imageBase64: string): Promise<string> {

FILE: lib/server/tencent-sign.ts
  type SignParams (line 3) | interface SignParams {
  function sha256hex (line 15) | function sha256hex(message: string): string {
  function getDate (line 21) | function getDate(timestamp: number): string {
  function sign (line 26) | function sign(params: SignParams): string {

FILE: lib/server/translate.ts
  function translateWithDeepSeek (line 4) | async function translateWithDeepSeek(text: string, targetLanguage: strin...
  function translateWithQwen (line 37) | async function translateWithQwen(text: string, targetLanguage: string) {
  function translateWithZhipu (line 69) | async function translateWithZhipu(text: string, targetLanguage: string) {
  function translateWithHunyuan (line 101) | async function translateWithHunyuan(text: string, targetLang: string) {
  function translateWith4oMini (line 133) | async function translateWith4oMini(text: string, targetLanguage: string) {
  function translateWithMinniMax (line 165) | async function translateWithMinniMax(text: string, targetLanguage: strin...
  function translateWithSiliconFlow (line 197) | async function translateWithSiliconFlow(text: string, targetLanguage: st...
  function translateWithClaude (line 228) | async function translateWithClaude(text: string, targetLanguage: string) {
  function translateWithKimiAPI (line 259) | async function translateWithKimiAPI(text: string, targetLanguage: string) {
  function translateWithStepAPI (line 299) | async function translateWithStepAPI(text: string, targetLanguage: string) {

FILE: lib/speech.ts
  type IWindow (line 3) | interface IWindow extends Window {
  class SpeechRecognitionService (line 7) | class SpeechRecognitionService {
    method constructor (line 11) | constructor() {
    method start (line 23) | public start(onResult: (text: string, isFinal: boolean) => void, onErr...
    method stop (line 55) | public stop() {

FILE: lib/step.ts
  function extractTextWithStep (line 1) | async function extractTextWithStep(image: string | File) {

FILE: lib/stripe.ts
  constant PLANS (line 18) | const PLANS = {

FILE: lib/tencent-asr.ts
  type Window (line 4) | interface Window {
  type TaskResponse (line 10) | interface TaskResponse {
  type StatusResponse (line 19) | interface StatusResponse {
  class TencentASRService (line 29) | class TencentASRService {
    method processQueue (line 33) | private async processQueue() {
    method addToQueue (line 50) | private addToQueue(task: () => Promise<void>) {
    method fileToBase64 (line 55) | private async fileToBase64(file: File): Promise<string> {
    method cleanTimestamps (line 69) | private cleanTimestamps(text: string): string {
    method recognizeAudio (line 74) | async recognizeAudio(
    method recognizeStream (line 158) | async recognizeStream(

FILE: lib/tencent-sign.ts
  type SignParams (line 3) | interface SignParams {
  function sha256 (line 15) | async function sha256(message: string): Promise<string> {
  function hmacSha256 (line 22) | async function hmacSha256(key: ArrayBuffer, message: string): Promise<Ar...
  function getDate (line 37) | function getDate(timestamp: number): string {
  function arrayBufferToHex (line 42) | function arrayBufferToHex(buffer: ArrayBuffer): string {
  function sign (line 48) | async function sign(params: SignParams): Promise<string> {

FILE: lib/tencent.ts
  constant MAX_RETRIES (line 5) | const MAX_RETRIES = 3
  constant RETRY_DELAY (line 6) | const RETRY_DELAY = 1000
  function retryWithDelay (line 8) | async function retryWithDelay<T>(fn: () => Promise<T>, retries = MAX_RET...
  function extractTextWithTencent (line 20) | async function extractTextWithTencent(imageBase64: string): Promise<stri...

FILE: lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
  function delay (line 8) | function delay(ms: number): Promise<void> {

FILE: lib/zhipu.ts
  constant ZHIPU_API_KEY (line 1) | const ZHIPU_API_KEY = process.env.NEXT_PUBLIC_ZHIPU_API_KEY
  constant API_URL (line 2) | const API_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'
  constant VISION_API_URL (line 3) | const VISION_API_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/comple...
  type Message (line 5) | interface Message {
  function base64UrlEncode (line 17) | function base64UrlEncode(str: string): string {
  function stringToUint8Array (line 25) | function stringToUint8Array(str: string): Uint8Array {
  function uint8ArrayToBase64Url (line 30) | function uint8ArrayToBase64Url(uint8Array: Uint8Array): string {
  function getZhipuToken (line 38) | async function getZhipuToken() {
  function translateWithZhipu (line 96) | async function translateWithZhipu(text: string, targetLanguage: string):...
  function extractVideoFrames (line 136) | async function extractVideoFrames(videoFile: File): Promise<string[]> {
  function analyzeVideoContent (line 150) | async function analyzeVideoContent(frames: string[]): Promise<string> {
  function extractTextWithZhipu (line 214) | async function extractTextWithZhipu(imageBase64: string): Promise<string> {
  function extractFileContent (line 273) | async function extractFileContent(file: File): Promise<string> {

FILE: types/alicloud.d.ts
  class Client (line 2) | class Client {
  class RecognizeVideoContentRequest (line 8) | class RecognizeVideoContentRequest {
  class GetAsyncJobResultRequest (line 12) | class GetAsyncJobResultRequest {
  class Config (line 18) | class Config {
  class RuntimeOptions (line 29) | class RuntimeOptions {
  type OSSOptions (line 35) | interface OSSOptions {
  type MultipartUploadOptions (line 52) | interface MultipartUploadOptions {
  type PutResult (line 58) | interface PutResult {
  class OSS (line 70) | class OSS {
  type STSOptions (line 81) | interface STSOptions {
  type Credentials (line 88) | interface Credentials {
  type AssumeRoleResponse (line 95) | interface AssumeRoleResponse {
  class STS (line 99) | class STS {
  type AssumeRoleRequest (line 114) | interface AssumeRoleRequest {
  type Credentials (line 121) | interface Credentials {
  type AssumeRoleResponse (line 128) | interface AssumeRoleResponse {
  class Client (line 135) | class Client {

FILE: types/next-auth.d.ts
  type User (line 5) | interface User {
  type Session (line 10) | interface Session {
  type JWT (line 16) | interface JWT {
Condensed preview — 146 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (559K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 373,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "README.md",
    "chars": 2606,
    "preview": "# AI 翻译助手\n\n一个功能强大的 AI 驱动的多语言翻译和内容处理平台。\n\n## 主要功能\n\n### 1. 多模态翻译\n- **文本翻译**:支持无限次免费文本翻译,多种语言互译\n- **图片识别**:支持图片内容识别和翻译,可处理多种"
  },
  {
    "path": "app/api/aliyun/oss/sts/route.ts",
    "chars": 2555,
    "preview": "import { NextResponse } from 'next/server'\nimport * as $OpenApi from '@alicloud/openapi-client'\nimport * as $STS20150401"
  },
  {
    "path": "app/api/aliyun/oss/upload/route.ts",
    "chars": 2021,
    "preview": "import { NextResponse } from 'next/server'\nimport OSS from 'ali-oss'\nimport { v4 as uuidv4 } from 'uuid'\n\nexport async f"
  },
  {
    "path": "app/api/aliyun/video-ocr/create/route.ts",
    "chars": 1883,
    "preview": "import { NextResponse } from 'next/server'\nimport RPCClient from '@alicloud/pop-core'\n\ninterface AsyncJobResult {\n  Requ"
  },
  {
    "path": "app/api/aliyun/video-ocr/query/route.ts",
    "chars": 3118,
    "preview": "import { NextResponse } from 'next/server'\nimport RPCClient from '@alicloud/pop-core'\n\ninterface AsyncJobQueryResult {\n "
  },
  {
    "path": "app/api/aliyun/video-ocr/status/route.ts",
    "chars": 6571,
    "preview": "import { NextResponse } from 'next/server'\nimport RPCClient from '@alicloud/pop-core'\n\ninterface VideoOCRResult {\n  OcrR"
  },
  {
    "path": "app/api/asr/aliyun/recognize/route.ts",
    "chars": 965,
    "preview": "import { NextResponse } from 'next/server';\n\nexport async function POST(request: Request) {\n  try {\n    const { audioUrl"
  },
  {
    "path": "app/api/asr/create/route.ts",
    "chars": 1687,
    "preview": "import { NextResponse } from 'next/server';\nimport { sign } from '@/lib/server/tencent-sign';\n\nconst endpoint = 'asr.ten"
  },
  {
    "path": "app/api/asr/status/route.ts",
    "chars": 1516,
    "preview": "import { NextResponse } from 'next/server';\nimport { sign } from '@/lib/server/tencent-sign';\n\nconst endpoint = 'asr.ten"
  },
  {
    "path": "app/api/auth/[...nextauth]/auth.ts",
    "chars": 4822,
    "preview": "import { AuthOptions } from 'next-auth'\nimport CredentialsProvider from 'next-auth/providers/credentials'\nimport GitHubP"
  },
  {
    "path": "app/api/auth/[...nextauth]/route.ts",
    "chars": 179,
    "preview": "import NextAuth from 'next-auth'\nimport { authOptions } from '@/app/api/auth/[...nextauth]/auth'\n\nconst handler = NextAu"
  },
  {
    "path": "app/api/auth/register/route.ts",
    "chars": 1015,
    "preview": "import { NextResponse } from 'next/server'\nimport { neon } from '@neondatabase/serverless'\nimport bcrypt from 'bcryptjs'"
  },
  {
    "path": "app/api/file/extract/route.ts",
    "chars": 12363,
    "preview": "import { NextResponse } from 'next/server'\nimport { Mistral } from '@mistralai/mistralai';\n\nconst KIMI_API_KEY = process"
  },
  {
    "path": "app/api/ocr/kimi/route.ts",
    "chars": 1686,
    "preview": "import { NextResponse } from 'next/server'\nimport OpenAI from 'openai'\n\nconst KIMI_API_KEY = process.env.NEXT_PUBLIC_KIM"
  },
  {
    "path": "app/api/ocr/route.ts",
    "chars": 1664,
    "preview": "import { NextResponse } from 'next/server'\nimport * as tencentcloud from 'tencentcloud-sdk-nodejs-ocr'\n\nconst OcrClient "
  },
  {
    "path": "app/api/ocr/step/route.ts",
    "chars": 2394,
    "preview": "import { NextResponse } from 'next/server'\nimport OpenAI from 'openai'\n\n// 设置较长的超时时间\nexport const maxDuration = 60; // 设"
  },
  {
    "path": "app/api/qwen/ocr/route.ts",
    "chars": 1652,
    "preview": "import { NextResponse } from 'next/server'\n\nif (!process.env.NEXT_PUBLIC_QWEN_API_KEY) {\n  throw new Error('Missing NEXT"
  },
  {
    "path": "app/api/qwen/translate/route.ts",
    "chars": 1610,
    "preview": "import { NextResponse } from 'next/server';\n\nexport const runtime = 'edge';\n\nexport async function POST(request: Request"
  },
  {
    "path": "app/api/register/route.ts",
    "chars": 1817,
    "preview": "import { NextResponse } from 'next/server'\nimport { neon } from '@neondatabase/serverless'\nimport bcrypt from 'bcryptjs'"
  },
  {
    "path": "app/api/subscription/route.ts",
    "chars": 1747,
    "preview": "import { getServerSession } from 'next-auth'\nimport { NextResponse } from 'next/server'\nimport { stripe, PLANS } from '@"
  },
  {
    "path": "app/api/tencent/ocr/route.ts",
    "chars": 1571,
    "preview": "import { NextResponse } from 'next/server';\n\n// 使用 require 导入腾讯云 SDK\nconst tencentcloud = require(\"tencentcloud-sdk-node"
  },
  {
    "path": "app/api/translate/claude/route.ts",
    "chars": 1400,
    "preview": "import { NextResponse } from 'next/server';\nimport OpenAI from 'openai';\n\nexport async function POST(request: Request) {"
  },
  {
    "path": "app/api/translate/kimi/route.ts",
    "chars": 1539,
    "preview": "import { NextResponse } from 'next/server';\nimport OpenAI from 'openai';\n\nexport async function POST(request: Request) {"
  },
  {
    "path": "app/api/translate/route.ts",
    "chars": 9971,
    "preview": "import OpenAI from 'openai';\nimport { sign } from '@/lib/server/tencent-sign';\nimport { translateWithKimiAPI } from '@/l"
  },
  {
    "path": "app/api/translate/siliconflow/route.ts",
    "chars": 1418,
    "preview": "import { NextResponse } from 'next/server';\nimport OpenAI from 'openai';\n\nexport async function POST(request: Request) {"
  },
  {
    "path": "app/api/translate/step/route.ts",
    "chars": 1483,
    "preview": "import { NextResponse } from 'next/server'\nimport OpenAI from 'openai'\n\nexport async function POST(req: Request) {\n  try"
  },
  {
    "path": "app/api/upload/route.ts",
    "chars": 958,
    "preview": "import { NextResponse } from 'next/server'\nimport { put } from '@vercel/blob'\n\nexport async function POST(request: Reque"
  },
  {
    "path": "app/api/user/info/route.ts",
    "chars": 6469,
    "preview": "import { getServerSession } from 'next-auth'\nimport { NextResponse } from 'next/server'\nimport { neon } from '@neondatab"
  },
  {
    "path": "app/api/user/update/route.ts",
    "chars": 978,
    "preview": "import { getServerSession } from 'next-auth'\nimport { NextResponse } from 'next/server'\nimport { neon } from '@neondatab"
  },
  {
    "path": "app/api/user/usage/route.ts",
    "chars": 5496,
    "preview": "import { NextResponse } from 'next/server'\nimport { getServerSession } from 'next-auth'\nimport { authOptions } from '@/a"
  },
  {
    "path": "app/api/webhook/route.ts",
    "chars": 5263,
    "preview": "import { headers } from 'next/headers'\nimport { NextResponse } from 'next/server'\nimport { stripe } from '@/lib/stripe'\n"
  },
  {
    "path": "app/globals.css",
    "chars": 3501,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --foreground-rgb: 0, 0, 0;\n  --background-start-rg"
  },
  {
    "path": "app/layout.tsx",
    "chars": 3222,
    "preview": "import './globals.css';\nimport { Inter } from 'next/font/google';\nimport { Providers } from \"./providers\";\nimport Google"
  },
  {
    "path": "app/login/error.tsx",
    "chars": 555,
    "preview": "'use client';\n\nimport { useSearchParams } from 'next/navigation';\nimport { useEffect } from 'react';\nimport { toast } fr"
  },
  {
    "path": "app/login/layout.tsx",
    "chars": 242,
    "preview": "import { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Sign in',\n  description: 'Sign in to your"
  },
  {
    "path": "app/login/page.tsx",
    "chars": 6125,
    "preview": "\"use client\";\n\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useSta"
  },
  {
    "path": "app/page.tsx",
    "chars": 19186,
    "preview": "\"use client\"\n\nimport Link from 'next/link'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { Button } from "
  },
  {
    "path": "app/pricing/layout.tsx",
    "chars": 880,
    "preview": "import type { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Pricing Plans - AI Translation Assis"
  },
  {
    "path": "app/pricing/page.tsx",
    "chars": 10050,
    "preview": "'use client'\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card } from \"@/components/ui/card\"\nimport { Check"
  },
  {
    "path": "app/profile/page.tsx",
    "chars": 12154,
    "preview": "\"use client\";\n\nimport { useEffect, useState } from \"react\"\nimport { useSession, signIn } from \"next-auth/react\"\nimport {"
  },
  {
    "path": "app/providers.tsx",
    "chars": 867,
    "preview": "\"use client\";\n\nimport { SessionProvider } from \"next-auth/react\";\nimport { useLanguage } from \"@/components/language-pro"
  },
  {
    "path": "app/register/layout.tsx",
    "chars": 241,
    "preview": "import { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Sign up',\n  description: 'Create your acc"
  },
  {
    "path": "app/register/page.tsx",
    "chars": 7419,
    "preview": "\"use client\";\n\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useSta"
  },
  {
    "path": "app/translate/layout.tsx",
    "chars": 826,
    "preview": "import type { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Online Translation - AI Translation "
  },
  {
    "path": "app/translate/page.tsx",
    "chars": 80975,
    "preview": "\"use client\"\n\nimport { useState, useCallback, useEffect, useRef } from 'react'\nimport { Upload, Image as ImageIcon, Lang"
  },
  {
    "path": "components/client-layout.tsx",
    "chars": 533,
    "preview": "\"use client\"\n\nimport { ThemeProvider } from '@/components/theme-provider';\nimport { LanguageProvider } from '@/component"
  },
  {
    "path": "components/footer.tsx",
    "chars": 2743,
    "preview": "\"use client\"\n\nimport { Github, Twitter, Globe, Chrome, MonitorSmartphone, Lock } from 'lucide-react'\nimport { Button } f"
  },
  {
    "path": "components/google-analytics.tsx",
    "chars": 634,
    "preview": "'use client'\n\nimport Script from 'next/script'\n\nconst GoogleAnalytics = () => {\n  const gaId = process.env.NEXT_PUBLIC_G"
  },
  {
    "path": "components/header.tsx",
    "chars": 6852,
    "preview": "\"use client\"\n\nimport { MoonIcon, SunIcon, Languages } from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport "
  },
  {
    "path": "components/language-provider.tsx",
    "chars": 1874,
    "preview": "\"use client\"\n\nimport { createContext, useContext, useState, useEffect } from \"react\"\nimport { useI18n } from \"@/lib/i18n"
  },
  {
    "path": "components/language-switcher.tsx",
    "chars": 1259,
    "preview": "\"use client\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  Dropdown"
  },
  {
    "path": "components/language-toggle.tsx",
    "chars": 912,
    "preview": "import { Languages } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  Drop"
  },
  {
    "path": "components/layout.tsx",
    "chars": 1229,
    "preview": "import { Languages, Sparkles } from 'lucide-react'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { ThemeT"
  },
  {
    "path": "components/subscription-dialog.tsx",
    "chars": 1960,
    "preview": "import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { Bu"
  },
  {
    "path": "components/testimonials.tsx",
    "chars": 2284,
    "preview": "\"use client\"\n\nimport { useEffect } from 'react'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { Card, Car"
  },
  {
    "path": "components/theme-provider.tsx",
    "chars": 326,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\"\nimport { "
  },
  {
    "path": "components/theme-toggle.tsx",
    "chars": 1314,
    "preview": "import { Moon, Sun } from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Button } from \"@/components/ui/"
  },
  {
    "path": "components/ui/accordion.tsx",
    "chars": 2004,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport {"
  },
  {
    "path": "components/ui/alert-dialog.tsx",
    "chars": 4459,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';\n\nim"
  },
  {
    "path": "components/ui/alert.tsx",
    "chars": 1595,
    "preview": "import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '"
  },
  {
    "path": "components/ui/aspect-ratio.tsx",
    "chars": 158,
    "preview": "'use client';\n\nimport * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';\n\nconst AspectRatio = AspectRatioPri"
  },
  {
    "path": "components/ui/avatar.tsx",
    "chars": 1430,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as AvatarPrimitive from '@radix-ui/react-avatar';\n\nimport { cn }"
  },
  {
    "path": "components/ui/badge.tsx",
    "chars": 1134,
    "preview": "import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '"
  },
  {
    "path": "components/ui/breadcrumb.tsx",
    "chars": 2724,
    "preview": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { ChevronRight, MoreHorizontal } fro"
  },
  {
    "path": "components/ui/button.tsx",
    "chars": 1846,
    "preview": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'cla"
  },
  {
    "path": "components/ui/calendar.tsx",
    "chars": 2633,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { DayPic"
  },
  {
    "path": "components/ui/card.tsx",
    "chars": 1905,
    "preview": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  R"
  },
  {
    "path": "components/ui/carousel.tsx",
    "chars": 6296,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from 'embla-car"
  },
  {
    "path": "components/ui/chart.tsx",
    "chars": 10551,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as RechartsPrimitive from 'recharts';\n\nimport { cn } from '@/lib"
  },
  {
    "path": "components/ui/checkbox.tsx",
    "chars": 1078,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { C"
  },
  {
    "path": "components/ui/collapsible.tsx",
    "chars": 335,
    "preview": "'use client';\n\nimport * as CollapsiblePrimitive from '@radix-ui/react-collapsible';\n\nconst Collapsible = CollapsiblePrim"
  },
  {
    "path": "components/ui/command.tsx",
    "chars": 4920,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport { type DialogProps } from '@radix-ui/react-dialog';\nimport { Comma"
  },
  {
    "path": "components/ui/context-menu.tsx",
    "chars": 7294,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as ContextMenuPrimitive from '@radix-ui/react-context-menu';\nimp"
  },
  {
    "path": "components/ui/dialog.tsx",
    "chars": 3871,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { X } f"
  },
  {
    "path": "components/ui/drawer.tsx",
    "chars": 3043,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport { Drawer as DrawerPrimitive } from 'vaul';\n\nimport { cn } from '@/"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "chars": 7343,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\ni"
  },
  {
    "path": "components/ui/form.tsx",
    "chars": 4150,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { Slot } "
  },
  {
    "path": "components/ui/hover-card.tsx",
    "chars": 1207,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as HoverCardPrimitive from '@radix-ui/react-hover-card';\n\nimport"
  },
  {
    "path": "components/ui/input-otp.tsx",
    "chars": 2185,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport { OTPInput, OTPInputContext } from 'input-otp';\nimport { Dot } fro"
  },
  {
    "path": "components/ui/input.tsx",
    "chars": 851,
    "preview": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nexport interface InputProps\n  extends React.InputHTM"
  },
  {
    "path": "components/ui/label.tsx",
    "chars": 733,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, ty"
  },
  {
    "path": "components/ui/menubar.tsx",
    "chars": 8025,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as MenubarPrimitive from '@radix-ui/react-menubar';\nimport { Che"
  },
  {
    "path": "components/ui/navigation-menu.tsx",
    "chars": 5067,
    "preview": "import * as React from 'react';\nimport * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';\nimport { cva"
  },
  {
    "path": "components/ui/pagination.tsx",
    "chars": 2772,
    "preview": "import * as React from 'react';\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';\n\nimport { cn }"
  },
  {
    "path": "components/ui/popover.tsx",
    "chars": 1253,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\n\nimport { cn"
  },
  {
    "path": "components/ui/progress.tsx",
    "chars": 798,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as ProgressPrimitive from '@radix-ui/react-progress';\n\nimport { "
  },
  {
    "path": "components/ui/radio-group.tsx",
    "chars": 1493,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimpor"
  },
  {
    "path": "components/ui/resizable.tsx",
    "chars": 1732,
    "preview": "'use client';\n\nimport { GripVertical } from 'lucide-react';\nimport * as ResizablePrimitive from 'react-resizable-panels'"
  },
  {
    "path": "components/ui/scroll-area.tsx",
    "chars": 1665,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';\n\nimpo"
  },
  {
    "path": "components/ui/select.tsx",
    "chars": 5652,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as SelectPrimitive from '@radix-ui/react-select';\nimport { Check"
  },
  {
    "path": "components/ui/separator.tsx",
    "chars": 777,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as SeparatorPrimitive from '@radix-ui/react-separator';\n\nimport "
  },
  {
    "path": "components/ui/sheet.tsx",
    "chars": 4305,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as SheetPrimitive from '@radix-ui/react-dialog';\nimport { cva, t"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "chars": 264,
    "preview": "import { cn } from '@/lib/utils';\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) "
  },
  {
    "path": "components/ui/slider.tsx",
    "chars": 1098,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as SliderPrimitive from '@radix-ui/react-slider';\n\nimport { cn }"
  },
  {
    "path": "components/ui/sonner.tsx",
    "chars": 902,
    "preview": "'use client';\n\nimport { useTheme } from 'next-themes';\nimport { Toaster as Sonner } from 'sonner';\n\ntype ToasterProps = "
  },
  {
    "path": "components/ui/switch.tsx",
    "chars": 1160,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as SwitchPrimitives from '@radix-ui/react-switch';\n\nimport { cn "
  },
  {
    "path": "components/ui/table.tsx",
    "chars": 2784,
    "preview": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n"
  },
  {
    "path": "components/ui/tabs.tsx",
    "chars": 1909,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as TabsPrimitive from '@radix-ui/react-tabs';\n\nimport { cn } fro"
  },
  {
    "path": "components/ui/textarea.tsx",
    "chars": 778,
    "preview": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nexport interface TextareaProps\n  extends React.Texta"
  },
  {
    "path": "components/ui/toast.tsx",
    "chars": 4858,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type"
  },
  {
    "path": "components/ui/toaster.tsx",
    "chars": 793,
    "preview": "\"use client\"\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from"
  },
  {
    "path": "components/ui/toggle-group.tsx",
    "chars": 1767,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';\nimp"
  },
  {
    "path": "components/ui/toggle.tsx",
    "chars": 1458,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as TogglePrimitive from '@radix-ui/react-toggle';\nimport { cva, "
  },
  {
    "path": "components/ui/tooltip.tsx",
    "chars": 1169,
    "preview": "'use client';\n\nimport * as React from 'react';\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip';\n\nimport { cn"
  },
  {
    "path": "components/ui/use-toast.ts",
    "chars": 3773,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"@/components/ui/"
  },
  {
    "path": "components.json",
    "chars": 417,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "hooks/use-toast.ts",
    "chars": 4005,
    "preview": "'use client';\n\n// Inspired by react-hot-toast library\nimport * as React from 'react';\n\nimport type { ToastActionElement,"
  },
  {
    "path": "lib/aliyun-oss-client.ts",
    "chars": 1890,
    "preview": "import OSS from 'ali-oss';\n\ninterface STSToken {\n  region: string;\n  bucket: string;\n  credentials: {\n    accessKeyId: s"
  },
  {
    "path": "lib/aliyun-oss-upload.ts",
    "chars": 1944,
    "preview": "import OSS from 'ali-oss';\n\ninterface STSToken {\n  region: string;\n  bucket: string;\n  credentials: {\n    accessKeyId: s"
  },
  {
    "path": "lib/aliyun-oss.ts",
    "chars": 572,
    "preview": "\"use client\"\n\nexport async function uploadToOSS(file: File): Promise<string> {\n  try {\n    const formData = new FormData"
  },
  {
    "path": "lib/aliyun-video-ocr.ts",
    "chars": 1839,
    "preview": "\"use client\"\n\ninterface VideoOCRResponse {\n  RequestId: string\n  Data: {\n    Status: string\n    Results: Array<{\n      T"
  },
  {
    "path": "lib/db/migrate.ts",
    "chars": 2666,
    "preview": "import { neon } from '@neondatabase/serverless'\nimport fs from 'fs'\nimport path from 'path'\nimport dotenv from 'dotenv'\n"
  },
  {
    "path": "lib/deepseek.ts",
    "chars": 2807,
    "preview": "\"use client\"\n\nconst API_URL = 'https://api.deepseek.com/v1/chat/completions'\n\n// 重试函数\nasync function retryWithDelay<T>(\n"
  },
  {
    "path": "lib/gemini.ts",
    "chars": 3993,
    "preview": "\"use client\"\n\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\n\nconst genAI = new GoogleGenerativeAI(process."
  },
  {
    "path": "lib/hooks/use-analytics.ts",
    "chars": 642,
    "preview": "'use client'\n\nexport const useAnalytics = () => {\n  const trackEvent = (\n    action: string,\n    category: string,\n    l"
  },
  {
    "path": "lib/hooks/use-quota.ts",
    "chars": 1267,
    "preview": "import { useState } from 'react'\nimport { useI18n } from '@/lib/i18n/use-translations'\n\ntype UsageType = 'text' | 'image"
  },
  {
    "path": "lib/i18n/locales/en.json",
    "chars": 21853,
    "preview": "{\n  \"tabs\": {\n    \"image\": \"Image Recognition\",\n    \"speech\": \"Speech Recognition\",\n    \"text\": \"Text Translation\",\n    "
  },
  {
    "path": "lib/i18n/locales/zh.json",
    "chars": 15224,
    "preview": "{\n  \"tabs\": {\n    \"image\": \"图片识别\",\n    \"speech\": \"语音识别\",\n    \"text\": \"文本翻译\",\n    \"video\": \"视频识别\",\n    \"file\": \"文件识别\",\n  "
  },
  {
    "path": "lib/i18n/translations.ts",
    "chars": 123,
    "preview": "import en from './locales/en.json'\nimport zh from './locales/zh.json'\n\nexport const translations = {\n  en,\n  zh,\n} as co"
  },
  {
    "path": "lib/i18n/use-translations.ts",
    "chars": 1544,
    "preview": "\"use client\"\n\nimport { create } from 'zustand'\nimport zhTranslations from './locales/zh.json'\nimport enTranslations from"
  },
  {
    "path": "lib/kimi.ts",
    "chars": 5233,
    "preview": "import { encode } from 'base64-arraybuffer'\n\n// 图片文字识别函数\nexport async function extractTextWithKimi(image: string): Promi"
  },
  {
    "path": "lib/languages.ts",
    "chars": 4491,
    "preview": "export interface Language {\n  code: string;\n  name: string;\n  nativeName: string;\n  category: string;\n}\n\nexport const la"
  },
  {
    "path": "lib/qwen.ts",
    "chars": 2250,
    "preview": "\"use client\"\n\n// 重试函数\nasync function retryWithDelay<T>(\n  fn: () => Promise<T>,\n  retries = 3,\n  delay = 1000,\n  backoff"
  },
  {
    "path": "lib/server/tencent-sign.ts",
    "chars": 2013,
    "preview": "import { createHmac, createHash } from 'crypto';\n\ninterface SignParams {\n  secretId: string;\n  secretKey: string;\n  endp"
  },
  {
    "path": "lib/server/translate.ts",
    "chars": 7844,
    "preview": "import OpenAI from 'openai';\n\n// 使用 DeepSeek API 进行文本翻译\nexport async function translateWithDeepSeek(text: string, target"
  },
  {
    "path": "lib/speech.ts",
    "chars": 1564,
    "preview": "\"use client\"\n\ninterface IWindow extends Window {\n  webkitSpeechRecognition: any;\n}\n\nexport class SpeechRecognitionServic"
  },
  {
    "path": "lib/step.ts",
    "chars": 568,
    "preview": "export async function extractTextWithStep(image: string | File) {\n  try {\n    const formData = new FormData();\n    formD"
  },
  {
    "path": "lib/stripe.ts",
    "chars": 1981,
    "preview": "import Stripe from 'stripe'\n\n// 服务器端 Stripe 实例\nexport const stripe = process.env.STRIPE_SECRET_KEY \n  ? new Stripe(proce"
  },
  {
    "path": "lib/tencent-asr.ts",
    "chars": 4937,
    "preview": "\"use client\"\n\ndeclare global {\n  interface Window {\n    SpeechRecognition: any;\n    webkitSpeechRecognition: any;\n  }\n}\n"
  },
  {
    "path": "lib/tencent-sign.ts",
    "chars": 2679,
    "preview": "\"use client\"\n\ninterface SignParams {\n  secretId: string;\n  secretKey: string;\n  endpoint: string;\n  service: string;\n  v"
  },
  {
    "path": "lib/tencent.ts",
    "chars": 1133,
    "preview": "\"use client\"\n\nimport { delay } from './utils'\n\nconst MAX_RETRIES = 3\nconst RETRY_DELAY = 1000\n\nasync function retryWithD"
  },
  {
    "path": "lib/utils.ts",
    "chars": 281,
    "preview": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: C"
  },
  {
    "path": "lib/zhipu.ts",
    "chars": 9076,
    "preview": "const ZHIPU_API_KEY = process.env.NEXT_PUBLIC_ZHIPU_API_KEY\nconst API_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/c"
  },
  {
    "path": "middleware.ts",
    "chars": 742,
    "preview": "import { withAuth } from \"next-auth/middleware\"\n\n// 导出中间件函数\nexport default withAuth(\n  // `withAuth` augments your `Requ"
  },
  {
    "path": "next.config.js",
    "chars": 175,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  eslint: {\n    ignoreDuringBuilds: true,\n  },\n  images: {"
  },
  {
    "path": "package.json",
    "chars": 3792,
    "preview": "{\n  \"name\": \"nextjs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next "
  },
  {
    "path": "postcss.config.js",
    "chars": 83,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "public/ads.txt",
    "chars": 58,
    "preview": "google.com, pub-9535069756501112, DIRECT, f08c47fec0942fa0"
  },
  {
    "path": "public/site.webmanifest",
    "chars": 539,
    "preview": "{\n  \"name\": \"AI Translation Assistant\",\n  \"short_name\": \"AI Translate\",\n  \"description\": \"All-in-one intelligent transla"
  },
  {
    "path": "tailwind.config.js",
    "chars": 2889,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    './pages/**/*.{ts"
  },
  {
    "path": "tailwind.config.ts",
    "chars": 2568,
    "preview": "import type { Config } from 'tailwindcss';\n\nconst config: Config = {\n  darkMode: ['class'],\n  content: [\n    './pages/**"
  },
  {
    "path": "tsconfig.json",
    "chars": 796,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n "
  },
  {
    "path": "types/alicloud.d.ts",
    "chars": 3006,
    "preview": "declare module '@alicloud/ocr-api20210707' {\n  export class Client {\n    constructor(config: any)\n    recognizeVideoCont"
  },
  {
    "path": "types/next-auth.d.ts",
    "chars": 278,
    "preview": "import NextAuth from \"next-auth\";\nimport { JWT } from \"next-auth/jwt\";\n\ndeclare module \"next-auth\" {\n  interface User {\n"
  }
]

About this extraction

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

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

Copied to clipboard!