feat: 实现多 OCR 提供商架构和完整设置页面

## 主要变更

### OCR 架构
- 新增多提供商 OCR 系统 (Tesseract.js, Baidu OCR, RapidOCR)
- 添加 Provider 基类接口和工厂模式
- 支持 provider 自动选择和降级处理
- 新增 RapidOCR Python HTTP 服务 (端口 8080)

### 路径修复
- 修复 Windows 平台路径解析问题
- 统一路径处理工具 (lib/path.ts)
- 修复 uploads 目录定位问题

### 设置页面重构
- 三个标签页:API 配置、OCR 配置、AI 配置
- API 服务器地址配置
- OCR 服务商配置(Tesseract.js, RapidOCR, 百度 OCR)
- AI 服务商配置(智谱 GLM, MiniMax, DeepSeek, Kimi, OpenAI, Anthropic)

### 端口配置
- 前端端口: 13056
- 后端端口: 13057

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
锦麟 王
2026-02-26 14:00:22 +08:00
parent 813df6c738
commit f8472987f0
23 changed files with 2321 additions and 275 deletions

84
backend/src/lib/path.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* 路径解析工具
* 解决开发环境下路径解析问题
*/
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
/**
* 获取项目根目录
* 通过从当前文件向上查找 package.json 来确定
*/
export function getProjectRoot(): string {
// 在开发环境使用 tsx 时,使用 process.cwd()
// 在构建后的环境,使用 __dirname 的方式
let currentDir: string;
try {
// ESM 模式下获取当前文件目录
const __filename = fileURLToPath(import.meta.url);
currentDir = path.dirname(__filename);
} catch {
// 回退到 process.cwd()
currentDir = process.cwd();
}
// Windows 路径处理(去除开头的 /
if (process.platform === 'win32' && currentDir.startsWith('/') && /^[a-zA-Z]:/.test(currentDir.slice(1))) {
currentDir = currentDir.substring(1);
}
// 从当前目录向上查找 package.json
let searchDir = currentDir;
for (let i = 0; i < 10; i++) {
const pkgPath = path.join(searchDir, 'package.json');
if (fs.existsSync(pkgPath)) {
return searchDir;
}
searchDir = path.dirname(searchDir);
}
// 如果找不到,回退到 process.cwd()
return process.cwd();
}
/**
* 获取上传目录的绝对路径
*/
export function getUploadsDir(): string {
const projectRoot = getProjectRoot();
return path.join(projectRoot, 'uploads');
}
/**
* 解析图片路径
* 将数据库中存储的路径 (/uploads/xxx.png) 解析为绝对路径
*/
export function resolveImagePath(imagePath: string): string {
// 在 Windows 上path.isAbsolute 会将 /uploads/... 认为是绝对路径
// 但这实际上是 Unix 风格的相对路径,需要特殊处理
const isWindowsAbsPath = process.platform === 'win32'
? /^[a-zA-Z]:\\/.test(imagePath) // Windows 真正的绝对路径如 C:\
: path.isAbsolute(imagePath);
if (isWindowsAbsPath) {
return imagePath;
}
// 处理 /uploads/ 开头的相对路径
if (imagePath.startsWith('/uploads/')) {
return path.join(getUploadsDir(), imagePath.replace('/uploads/', ''));
}
// 其他相对路径,使用项目根目录
return path.join(getProjectRoot(), imagePath);
}
/**
* 生成存储到数据库的路径
*/
export function generateDbPath(filename: string): string {
return `/uploads/${filename}`;
}