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('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('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 SubtitlesChineseResults?: Record SubtitlesEnglishResults?: Record 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('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 ( {children} ); } ================================================ 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 (

{t('auth.login.title')}

{t('auth.login.subtitle')}

{t('auth.login.or')}
setEmail(e.target.value)} disabled={loading} />
setPassword(e.target.value)} disabled={loading} />
{t('auth.login.noAccount')}{' '} {t('auth.signUp')}
) } ================================================ 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: , title: t('landing.features.text.title'), description: t('landing.features.text.description'), }, { icon: , title: t('landing.features.image.title'), description: t('landing.features.image.description'), }, { icon: , title: t('landing.features.pdf.title'), description: t('landing.features.pdf.description'), }, { icon: , title: t('landing.features.speech.title'), description: t('landing.features.speech.description'), }, { icon: