Introduces core backend and frontend infrastructure for a PDF translation interface. Adds API endpoints for translation, PDF testing, and AI provider testing; implements PDF text extraction, cost tracking, and pricing logic in the lib directory; adds reusable UI components; and provides comprehensive documentation (SDD, environment setup, Claude instructions). Updates Tailwind and global styles, and includes a sample test PDF and configuration files.
28 KiB
28 KiB
技術設計文件 (Technical Design Document)
1. 技術概述
1.1 系統架構
PDF Translation Interface 採用現代化的全端技術堆疊,基於 Next.js 15 App Router 架構,提供高效能的 PDF 文件翻譯服務。
1.2 核心技術選擇理由
前端技術選擇
- Next.js 15 App Router: 最新的 React 框架,提供更好的 SEO 和效能
- React 19: 最新的狀態管理和渲染優化
- TypeScript: 強型別系統,提高程式碼品質和維護性
- Tailwind CSS v4: 現代化的 CSS 框架,提供快速開發和優秀的自訂性
後端技術選擇
- Next.js API Routes: 與前端整合,簡化部署和維護
- pdf-parse + pdf2json: 多層備援的 PDF 文字擷取策略
- Tesseract.js: 客戶端 OCR,減少伺服器負載
- PDFKit: 強大的 PDF 生成能力,支援 Unicode
2. 系統架構詳細設計
2.1 前端架構
// 主要目錄結構
/app // Next.js 15 App Router
├── globals.css // 全域樣式
├── layout.tsx // 根佈局
├── page.tsx // 首頁
└── api/ // API 路由
└── translate/
└── route.ts // 翻譯 API 端點
/components // React 元件
├── ui/ // 基礎 UI 元件 (shadcn/ui)
│ ├── button.tsx
│ ├── card.tsx
│ ├── select.tsx
│ ├── label.tsx
│ └── checkbox.tsx
└── pdf-translator.tsx // 主要應用程式元件
/lib // 工具函式與邏輯
├── utils.ts // 通用工具函式
├── pdf-processor.ts // PDF 處理核心邏輯
├── pdf-to-image.ts // PDF 轉圖片功能
└── cost-tracker.ts // 費用追蹤系統
2.2 狀態管理架構
// PDFTranslator 元件狀態設計
interface PDFTranslatorState {
// 檔案處理
file: File | null
isDragging: boolean
// 語言設定
sourceLanguage: string
targetLanguage: string
// 翻譯流程
isTranslating: boolean
translatedText: string
translatedPDFBase64: string
generatePDF: boolean
// 語音功能
isPlaying: boolean
isPaused: boolean
speechSupported: boolean
selectedVoice: string
availableVoices: SpeechSynthesisVoice[]
speechRate: number
speechVolume: number
// 費用追蹤
tokenUsage: TokenUsage | null
cost: CostInfo | null
model: ModelInfo | null
costSummary: CostSummary | null
}
3. 核心模組技術實作
3.1 PDF 處理模組 (pdf-processor.ts)
3.1.1 多層文字擷取策略
export async function extractTextFromPDF(buffer: Buffer): Promise<PDFProcessResult> {
// 第一層:pdf-parse (主要方法)
try {
const pdfParseModule = await import('pdf-parse')
const result = await pdfParseModule(buffer)
if (result.text?.trim().length > 10) {
return { text: result.text, pageCount: result.numpages, isScanned: false }
}
} catch (error) {
console.log('pdf-parse failed, falling back to pdf2json')
}
// 第二層:pdf2json (備援方法)
try {
const PDFParser = require('pdf2json')
const pdfParser = new PDFParser()
const parseResult = await new Promise((resolve, reject) => {
pdfParser.on('pdfParser_dataReady', (pdfData: any) => {
let text = ''
if (pdfData.Pages) {
for (const page of pdfData.Pages) {
if (page.Texts) {
for (const textItem of page.Texts) {
if (textItem.R) {
for (const run of textItem.R) {
if (run.T) {
text += decodeURIComponent(run.T) + ' '
}
}
}
}
}
text += '\n'
}
}
resolve(text.trim())
})
pdfParser.on('pdfParser_dataError', reject)
pdfParser.parseBuffer(buffer)
})
if (parseResult && parseResult.length > 10) {
return { text: parseResult, pageCount: 1, isScanned: false }
}
} catch (error) {
console.log('pdf2json failed, PDF may be scanned')
}
// 第三層:標記為掃描文件,需要 OCR
return { text: '', pageCount: 1, isScanned: true }
}
3.1.2 智慧 PDF 生成
export async function generateTranslatedPDF(
translatedText: string,
originalMetadata?: any,
targetLanguage?: string
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
// 中文字元檢測
const hasChinese = /[\u4e00-\u9fff]/.test(translatedText)
if (hasChinese) {
// 中文內容:使用智慧後備描述系統
return await generateChinesePDF(pdfDoc, translatedText, targetLanguage)
} else {
// 非中文內容:標準 PDF 生成
return await generateStandardPDF(pdfDoc, translatedText)
}
}
async function generateChinesePDF(
pdfDoc: PDFDocument,
text: string,
targetLanguage?: string
): Promise<Uint8Array> {
const page = pdfDoc.addPage()
const { width, height } = page.getSize()
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
// 添加重要提示
page.drawText('IMPORTANT: Full Chinese translation is available in the', {
x: 50, y: height - 100, size: 11, font, color: rgb(0.8, 0.4, 0.0)
})
page.drawText('text output above this PDF download button.', {
x: 50, y: height - 115, size: 11, font, color: rgb(0.8, 0.4, 0.0)
})
// 處理中文字元並提供有意義的描述
const lines = text.split('\n')
let yPosition = height - 150
for (const line of lines) {
const processedLine = processChineseText(line)
try {
page.drawText(processedLine, {
x: 50, y: yPosition, size: 12, font, color: rgb(0, 0, 0)
})
} catch (error) {
// WinAnsi 編碼失敗時的後備處理
const fallbackDescription = generateContentDescription(line)
page.drawText(fallbackDescription, {
x: 50, y: yPosition, size: 12, font, color: rgb(0.3, 0.3, 0.3)
})
}
yPosition -= 15
}
return await pdfDoc.save()
}
function generateContentDescription(chineseText: string): string {
// 基於內容提供有意義的英文描述
if (chineseText.includes('測試')) return 'Testing PDF processing'
if (chineseText.includes('文字提取')) return 'Text extraction functionality'
if (chineseText.includes('翻譯')) return 'Translation process'
return 'Translated Chinese content'
}
3.2 費用追蹤模組 (cost-tracker.ts)
interface CostSession {
id: string
timestamp: Date
provider: string
model: string
tokenUsage: {
prompt: number
completion: number
total: number
}
cost: {
inputCost: number
outputCost: number
totalCost: number
currency: string
}
}
interface CostSummary {
totalSessions: number
totalTokens: number
totalCost: number
currency: string
byProvider: Record<string, {
sessions: number
tokens: number
cost: number
}>
}
class CostTracker {
private readonly STORAGE_KEY = 'pdf-translator-costs'
addCostSession(session: CostSession): CostSummary {
const sessions = this.getCostSessions()
sessions.push(session)
if (typeof window !== 'undefined') {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(sessions))
}
return this.calculateSummary(sessions)
}
getCostSummary(): CostSummary {
const sessions = this.getCostSessions()
return this.calculateSummary(sessions)
}
private calculateSummary(sessions: CostSession[]): CostSummary {
return sessions.reduce((summary, session) => ({
totalSessions: summary.totalSessions + 1,
totalTokens: summary.totalTokens + session.tokenUsage.total,
totalCost: summary.totalCost + session.cost.totalCost,
currency: session.cost.currency,
byProvider: {
...summary.byProvider,
[session.provider]: {
sessions: (summary.byProvider[session.provider]?.sessions || 0) + 1,
tokens: (summary.byProvider[session.provider]?.tokens || 0) + session.tokenUsage.total,
cost: (summary.byProvider[session.provider]?.cost || 0) + session.cost.totalCost
}
}
}), {
totalSessions: 0,
totalTokens: 0,
totalCost: 0,
currency: 'USD',
byProvider: {}
})
}
}
export const costTracker = new CostTracker()
3.3 語音播放模組
// 語音功能整合在 PDFTranslator 元件中
const playText = () => {
if (!speechSupported || !translatedText) return
// 恢復播放
if (isPaused) {
speechSynthesis.resume()
setIsPaused(false)
setIsPlaying(true)
return
}
// 新的播放
speechSynthesis.cancel()
const utterance = new SpeechSynthesisUtterance(translatedText)
// 語音配置
if (selectedVoice) {
const voice = availableVoices.find(v => v.name === selectedVoice)
if (voice) utterance.voice = voice
}
utterance.rate = speechRate
utterance.volume = speechVolume
utterance.pitch = 1.0
// 事件處理
utterance.onstart = () => setIsPlaying(true)
utterance.onend = () => { setIsPlaying(false); setIsPaused(false) }
utterance.onerror = (event) => {
console.error('Speech synthesis error:', event.error)
setIsPlaying(false)
setIsPaused(false)
}
speechSynthesis.speak(utterance)
}
// 自動語音選擇
const findPreferredVoice = (voices: SpeechSynthesisVoice[], langCode: string) => {
const langMap: Record<string, string[]> = {
'zh-TW': ['zh-TW', 'zh-HK', 'zh'],
'zh-CN': ['zh-CN', 'zh'],
'en': ['en-US', 'en-GB', 'en'],
'ja': ['ja-JP', 'ja'],
'ko': ['ko-KR', 'ko'],
// ... 其他語言映射
}
const targetLangs = langMap[langCode] || [langCode]
for (const targetLang of targetLangs) {
const voice = voices.find(v => v.lang.startsWith(targetLang))
if (voice) return voice
}
return voices[0] // 後備選項
}
4. API 設計與實作
4.1 翻譯 API (app/api/translate/route.ts)
export async function POST(request: Request) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
const sourceLanguage = formData.get('sourceLanguage') as string
const targetLanguage = formData.get('targetLanguage') as string
const returnPDF = formData.get('returnPDF') === 'true'
// 檔案驗證
if (!file || file.type !== 'application/pdf') {
return NextResponse.json({ error: '請上傳有效的 PDF 檔案' }, { status: 400 })
}
if (file.size > 10 * 1024 * 1024) { // 10MB 限制
return NextResponse.json({ error: '檔案大小不能超過 10MB' }, { status: 413 })
}
// PDF 文字擷取
const buffer = Buffer.from(await file.arrayBuffer())
const result = await extractTextFromPDF(buffer)
if (!result.text.trim()) {
return NextResponse.json({
error: 'PDF 文字提取失敗,可能是掃描檔案或加密文件'
}, { status: 400 })
}
// AI 翻譯
const { translatedText, tokenUsage, cost, model } = await translateText(
result.text,
sourceLanguage,
targetLanguage
)
// 費用追蹤
const costSession = {
id: generateId(),
timestamp: new Date(),
provider: model.provider,
model: model.name,
tokenUsage,
cost
}
// PDF 生成(可選)
let pdfBase64: string | undefined
if (returnPDF) {
const pdfBytes = await generateTranslatedPDF(translatedText, result.metadata, targetLanguage)
pdfBase64 = Buffer.from(pdfBytes).toString('base64')
}
return NextResponse.json({
translatedText,
pdfBase64,
tokenUsage: {
...tokenUsage,
formattedCounts: {
prompt: formatNumber(tokenUsage.prompt),
completion: formatNumber(tokenUsage.completion),
total: formatNumber(tokenUsage.total)
}
},
cost: {
...cost,
formattedCost: formatCost(cost.totalCost, cost.currency)
},
model: {
name: model.name,
provider: model.provider,
displayName: model.displayName
},
costSession
})
} catch (error) {
console.error('Translation error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : '翻譯過程中發生錯誤' },
{ status: 500 }
)
}
}
4.2 AI 翻譯整合
import { createOpenAI } from '@ai-sdk/openai'
import { generateText } from 'ai'
const deepseek = createOpenAI({
apiKey: process.env.DEEPSEEK_API_KEY!,
baseURL: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1',
})
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY!,
})
export async function translateText(
text: string,
sourceLanguage: string,
targetLanguage: string
) {
const provider = process.env.AI_PROVIDER || 'deepseek'
const model = provider === 'deepseek'
? deepseek(process.env.DEEPSEEK_MODEL || 'deepseek-chat')
: openai(process.env.OPENAI_MODEL || 'gpt-4o-mini')
const prompt = createTranslationPrompt(text, sourceLanguage, targetLanguage)
const result = await generateText({
model,
prompt,
maxTokens: 4000,
temperature: 0.3,
})
// Token 使用量計算
const tokenUsage = {
prompt: result.usage?.promptTokens || 0,
completion: result.usage?.completionTokens || 0,
total: result.usage?.totalTokens || 0
}
// 費用計算
const cost = calculateCost(tokenUsage, provider, model.modelId)
return {
translatedText: result.text,
tokenUsage,
cost,
model: {
name: model.modelId,
provider,
displayName: getModelDisplayName(model.modelId, provider)
}
}
}
function createTranslationPrompt(text: string, source: string, target: string): string {
const targetLang = LANGUAGE_NAMES[target] || target
const sourceLang = source === 'auto' ? '自動偵測' : LANGUAGE_NAMES[source] || source
return `請將以下${sourceLang}文字翻譯成${targetLang},保持原文的格式和段落結構,確保翻譯準確且自然:
${text}
請直接提供翻譯結果,不需要額外說明。`
}
5. UI/UX 技術實作
5.1 文清楓風格設計系統
// Tailwind CSS 自訂配置
const designTokens = {
colors: {
// 文清楓主色調
primary: {
amber: 'rgb(245 158 11)', // amber-500
green: 'rgb(34 197 94)', // green-500
teal: 'rgb(20 184 166)', // teal-500
},
// 背景漸變
background: {
light: 'from-amber-50 via-green-50 to-teal-50',
dark: 'dark:from-slate-800 dark:via-slate-900 dark:to-slate-800',
},
// 裝飾元素
decoration: {
circles: [
'bg-amber-200 blur-xl',
'bg-green-200 blur-xl',
'bg-teal-200 blur-xl',
'bg-amber-300 blur-xl'
]
}
},
spacing: {
responsive: {
mobile: 'p-4 gap-4',
tablet: 'sm:p-6 sm:gap-6',
desktop: 'lg:p-8 lg:gap-8'
}
}
}
5.2 響應式設計實作
// 響應式斷點策略
const breakpoints = {
sm: '640px', // 手機橫向
md: '768px', // 平板直向
lg: '1024px', // 平板橫向
xl: '1280px', // 桌面
'2xl': '1536px' // 大桌面
}
// 響應式元件設計模式
const ResponsiveComponent = () => (
<div className="
grid grid-cols-1 xl:grid-cols-2
gap-4 sm:gap-6 lg:gap-8
p-4 sm:p-6 lg:p-8
">
<Card className="
p-4 sm:p-6 lg:p-8
text-sm sm:text-base lg:text-lg
">
{/* 內容 */}
</Card>
</div>
)
// 行動裝置優化
const MobileOptimizedButton = ({ children, fullText, iconText }) => (
<Button className="
text-xs sm:text-sm lg:text-base
px-2 sm:px-4 lg:px-6
h-8 sm:h-10 lg:h-12
">
<Icon className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">{fullText}</span>
<span className="sm:hidden">{iconText}</span>
</Button>
)
5.3 拖拽上傳實作
const DragDropUpload = () => {
const [isDragging, setIsDragging] = useState(false)
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = () => {
setIsDragging(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFile = e.dataTransfer.files[0]
if (droppedFile && droppedFile.type === 'application/pdf') {
setFile(droppedFile)
} else {
alert('目前僅支援 PDF 文件')
}
}
return (
<div
className={`
border-2 border-dashed rounded-xl p-6 sm:p-8 lg:p-12
text-center transition-all duration-300
${isDragging
? 'border-green-500 bg-green-50/50 shadow-lg'
: 'border-amber-300 hover:border-green-400'
}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 拖拽區域內容 */}
</div>
)
}
6. 效能優化策略
6.1 前端效能優化
// 1. 元件懶加載
const PDFTranslator = dynamic(() => import('@/components/pdf-translator'), {
loading: () => <div className="animate-pulse">載入中...</div>,
ssr: false // 避免語音 API 的 SSR 問題
})
// 2. 記憶化計算
const memoizedCostSummary = useMemo(() => {
return costTracker.getCostSummary()
}, [costSummary])
// 3. 防抖動處理
const debouncedSpeechRateChange = useCallback(
debounce((rate: number) => setSpeechRate(rate), 300),
[]
)
// 4. 批次狀態更新
const updateTranslationResult = useCallback((data: TranslationResult) => {
// 批次更新避免多次重渲染
setTranslatedText(data.translatedText)
setTranslatedPDFBase64(data.pdfBase64 || '')
setTokenUsage(data.tokenUsage)
setCost(data.cost)
setModel(data.model)
if (data.costSession) {
const updatedSummary = costTracker.addCostSession(data.costSession)
setCostSummary(updatedSummary)
}
}, [])
6.2 後端效能優化
// 1. 串流處理大檔案
export async function processLargeFile(buffer: Buffer) {
const stream = new Readable({
read() {
// 分塊處理邏輯
}
})
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
stream.on('data', chunk => chunks.push(chunk))
stream.on('end', () => resolve(Buffer.concat(chunks)))
stream.on('error', reject)
})
}
// 2. PDF 處理快取
const pdfCache = new Map<string, PDFProcessResult>()
export async function cachedExtractTextFromPDF(buffer: Buffer): Promise<PDFProcessResult> {
const hash = createHash('md5').update(buffer).digest('hex')
if (pdfCache.has(hash)) {
return pdfCache.get(hash)!
}
const result = await extractTextFromPDF(buffer)
pdfCache.set(hash, result)
// 限制快取大小
if (pdfCache.size > 100) {
const firstKey = pdfCache.keys().next().value
pdfCache.delete(firstKey)
}
return result
}
// 3. AI API 請求優化
const rateLimiter = new Map<string, number>()
export async function rateLimitedTranslation(
text: string,
source: string,
target: string
) {
const key = `${source}-${target}`
const lastRequest = rateLimiter.get(key) || 0
const now = Date.now()
if (now - lastRequest < 1000) { // 1秒限制
await new Promise(resolve => setTimeout(resolve, 1000 - (now - lastRequest)))
}
rateLimiter.set(key, Date.now())
return await translateText(text, source, target)
}
7. 錯誤處理策略
7.1 分層錯誤處理
// 1. API 層錯誤處理
export class TranslationError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message)
this.name = 'TranslationError'
}
}
export class PDFProcessingError extends TranslationError {
constructor(message: string) {
super(message, 'PDF_PROCESSING_ERROR', 400)
}
}
export class AIServiceError extends TranslationError {
constructor(message: string, provider: string) {
super(`${provider} 服務錯誤: ${message}`, 'AI_SERVICE_ERROR', 502)
}
}
// 2. 全域錯誤處理中間件
export async function errorHandler(
error: Error,
request: Request
): Promise<NextResponse> {
console.error('API Error:', error)
if (error instanceof TranslationError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
)
}
// 未知錯誤
return NextResponse.json(
{ error: '內部伺服器錯誤', code: 'INTERNAL_ERROR' },
{ status: 500 }
)
}
// 3. 前端錯誤邊界
export function ErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundaryComponent
fallback={({ error, resetErrorBoundary }) => (
<div className="text-center p-8">
<h2 className="text-xl font-semibold mb-4">系統發生錯誤</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<Button onClick={resetErrorBoundary}>重新載入</Button>
</div>
)}
>
{children}
</ErrorBoundaryComponent>
)
}
7.2 使用者友善的錯誤提示
const errorMessages = {
PDF_TOO_LARGE: '檔案大小超過 10MB 限制,請選擇較小的檔案',
PDF_CORRUPTED: 'PDF 檔案損壞或加密,無法處理',
PDF_NO_TEXT: 'PDF 中未找到可擷取的文字,可能為純圖片檔案',
AI_QUOTA_EXCEEDED: 'AI 服務配額已達上限,請稍後再試',
AI_RATE_LIMITED: '請求過於頻繁,請稍等片刻後再試',
NETWORK_ERROR: '網路連線失敗,請檢查網路狀況',
UNSUPPORTED_LANGUAGE: '不支援的語言組合'
}
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return errorMessages[error.message] || error.message
}
return '發生未知錯誤,請重新嘗試'
}
8. 測試策略
8.1 單元測試
// PDF 處理測試
describe('PDF Processing', () => {
test('should extract text from text-based PDF', async () => {
const mockBuffer = Buffer.from('mock-pdf-content')
const result = await extractTextFromPDF(mockBuffer)
expect(result.text).toBeDefined()
expect(result.pageCount).toBeGreaterThan(0)
expect(result.isScanned).toBe(false)
})
test('should handle corrupted PDF gracefully', async () => {
const corruptedBuffer = Buffer.from('invalid-pdf')
await expect(extractTextFromPDF(corruptedBuffer))
.rejects.toThrow(PDFProcessingError)
})
})
// 費用追蹤測試
describe('Cost Tracking', () => {
beforeEach(() => {
localStorage.clear()
})
test('should calculate cost summary correctly', () => {
const session: CostSession = {
id: 'test-1',
timestamp: new Date(),
provider: 'deepseek',
model: 'deepseek-chat',
tokenUsage: { prompt: 100, completion: 50, total: 150 },
cost: { inputCost: 0.001, outputCost: 0.002, totalCost: 0.003, currency: 'USD' }
}
const summary = costTracker.addCostSession(session)
expect(summary.totalSessions).toBe(1)
expect(summary.totalTokens).toBe(150)
expect(summary.totalCost).toBe(0.003)
})
})
8.2 整合測試
// API 端點測試
describe('/api/translate', () => {
test('should translate PDF successfully', async () => {
const formData = new FormData()
formData.append('file', new File(['mock-pdf'], 'test.pdf', { type: 'application/pdf' }))
formData.append('sourceLanguage', 'zh-TW')
formData.append('targetLanguage', 'en')
formData.append('returnPDF', 'true')
const response = await fetch('/api/translate', {
method: 'POST',
body: formData
})
expect(response.status).toBe(200)
const data = await response.json()
expect(data.translatedText).toBeDefined()
expect(data.tokenUsage).toBeDefined()
expect(data.cost).toBeDefined()
})
})
8.3 端對端測試
// Playwright E2E 測試
test('complete translation workflow', async ({ page }) => {
await page.goto('/')
// 上傳檔案
await page.setInputFiles('input[type="file"]', 'test-files/sample.pdf')
// 選擇語言
await page.selectOption('[data-testid="source-language"]', 'zh-TW')
await page.selectOption('[data-testid="target-language"]', 'en')
// 開始翻譯
await page.click('[data-testid="translate-button"]')
// 等待結果
await page.waitForSelector('[data-testid="translation-result"]')
// 驗證結果
const result = await page.textContent('[data-testid="translation-result"]')
expect(result).toBeTruthy()
// 測試語音播放
await page.click('[data-testid="play-button"]')
await page.waitForTimeout(1000)
await page.click('[data-testid="pause-button"]')
})
9. 部署與監控
9.1 Vercel 部署配置
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"installCommand": "npm install",
"framework": "nextjs",
"functions": {
"app/api/translate/route.ts": {
"maxDuration": 30
}
},
"env": {
"NODE_ENV": "production"
}
}
9.2 效能監控
// 自訂效能監控
export class PerformanceMonitor {
static measureApiCall(name: string) {
const start = performance.now()
return {
end: (status: 'success' | 'error') => {
const duration = performance.now() - start
// 發送監控數據
if (typeof window !== 'undefined') {
navigator.sendBeacon('/api/metrics', JSON.stringify({
name,
duration,
status,
timestamp: Date.now()
}))
}
}
}
}
}
// 使用範例
export async function monitoredTranslation(params: TranslationParams) {
const monitor = PerformanceMonitor.measureApiCall('translation')
try {
const result = await translateText(params)
monitor.end('success')
return result
} catch (error) {
monitor.end('error')
throw error
}
}
10. 安全性實作
10.1 輸入驗證
import { z } from 'zod'
const TranslationRequestSchema = z.object({
file: z.instanceof(File)
.refine(file => file.type === 'application/pdf', '僅支援 PDF 檔案')
.refine(file => file.size <= 10 * 1024 * 1024, '檔案大小不能超過 10MB'),
sourceLanguage: z.enum(SUPPORTED_LANGUAGES),
targetLanguage: z.enum(SUPPORTED_LANGUAGES),
returnPDF: z.boolean().optional()
})
export async function validateRequest(formData: FormData) {
const data = {
file: formData.get('file'),
sourceLanguage: formData.get('sourceLanguage'),
targetLanguage: formData.get('targetLanguage'),
returnPDF: formData.get('returnPDF') === 'true'
}
return TranslationRequestSchema.parse(data)
}
10.2 API 金鑰管理
// 環境變數驗證
const envSchema = z.object({
DEEPSEEK_API_KEY: z.string().min(1, 'DeepSeek API key is required'),
OPENAI_API_KEY: z.string().optional(),
AI_PROVIDER: z.enum(['deepseek', 'openai']).default('deepseek'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development')
})
export const env = envSchema.parse(process.env)
// API 金鑰輪換
class APIKeyManager {
private keys: string[]
private currentIndex = 0
constructor(keys: string[]) {
this.keys = keys.filter(Boolean)
if (this.keys.length === 0) {
throw new Error('No valid API keys provided')
}
}
getKey(): string {
const key = this.keys[this.currentIndex]
this.currentIndex = (this.currentIndex + 1) % this.keys.length
return key
}
}
11. 未來技術升級計畫
11.1 短期升級 (1-3 個月)
- 實作 Redis 快取系統
- 添加請求速率限制
- 整合 Sentry 錯誤監控
- 實作批次檔案處理
11.2 中期升級 (3-6 個月)
- 實作 WebSocket 即時進度更新
- 整合更多 AI 模型 (Claude, Gemini)
- 實作用戶認證系統
- 添加翻譯歷史功能
11.3 長期升級 (6-12 個月)
- 實作分散式處理系統
- 整合專業 CAT 工具
- 實作協作翻譯功能
- 添加 API 開放平台
11.4 技術債務管理
- 重構 PDF 處理模組為微服務
- 實作完整的測試覆蓋率 (目標 >90%)
- 優化 TypeScript 型別定義
- 實作自動化效能測試
本技術設計文件將隨著系統演進持續更新,確保技術架構的可維護性和可擴展性。