feat: add recipe translation tool with AI translation and settings management

- Add Word document parsing and translation export
- Add database configuration management
- Add AI/LLM translation configuration
- Add translation vocabulary management
- Add export settings (colors, page orientation, headers)
- Separate database and AI configurations
- Add debounced search to prevent database overload
This commit is contained in:
2026-04-12 23:50:54 +08:00
parent 57e119635e
commit a249fb038c
24 changed files with 3383 additions and 70 deletions
+139
View File
@@ -0,0 +1,139 @@
import OpenAI from "openai";
// AI 翻译配置
interface AIConfig {
apiKey: string;
baseUrl?: string;
model?: string;
}
// 默认配置(从环境变量)
function getDefaultConfig(): AIConfig {
return {
apiKey: process.env.AI_API_KEY || "",
baseUrl: process.env.AI_BASE_URL || undefined,
model: process.env.AI_MODEL || "gpt-3.5-turbo",
};
}
// 模块级配置存储
let savedConfig: AIConfig | null = null;
// 设置保存的配置
export function setAIConfig(config: AIConfig) {
savedConfig = config;
}
// 获取配置(优先使用保存的配置)
export function getAIConfig(): AIConfig {
return savedConfig || getDefaultConfig();
}
// 解析翻译结果
function parseTranslation(content: string): string | null {
const patterns = [
/english\s*:\s*['"]([^'"]+)['"]/i,
/english\s*[:\s]+([^,'"\n]+)/i,
/['"]([^'"]+)['"]\s*[,}].*english/i,
/^([^,]+)\s*,\s*english\s*:\s*['"]([^'"]+)['"]/i,
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
return match[1]?.trim() || null;
}
}
return null;
}
// 翻译单个词组(带重试)
export async function translateWithAI(
word: string,
config?: AIConfig,
maxRetries: number = 3
): Promise<string | null> {
const aiConfig = config || getAIConfig();
if (!aiConfig.apiKey) {
throw new Error("AI API Key 未配置");
}
const client = new OpenAI({
apiKey: aiConfig.apiKey,
baseURL: aiConfig.baseUrl,
});
const prompt = `你是一个专业的中文到英文翻译助手。
任务:将给定的中文词组翻译成英文。
严格要求:
1. 必须严格返回 JSON 格式:{"word":"中文原文","english":"英文翻译"}
2. 不要返回任何其他内容,不要解释
3. 只返回这一行 JSON
4. 翻译要简洁、准确
中文词组:${word}
请立即返回 JSON 结果:`;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const completion = await client.chat.completions.create({
model: aiConfig.model || "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "你是一个严格遵循指令的翻译助手。必须只返回要求的 JSON 格式,不要返回任何其他内容。",
},
{ role: "user", content: prompt },
],
temperature: 0,
max_tokens: 50,
});
const content = completion.choices[0]?.message?.content || "";
const translation = parseTranslation(content);
if (translation && translation.length > 0) {
return translation;
}
// Fallback: 提取英文单词
const fallbackMatch = content.match(/[a-zA-Z][a-zA-Z\s-]*/);
if (fallbackMatch) {
const fallback = fallbackMatch[0].trim();
if (fallback.length > 1) {
return fallback;
}
}
} catch (error) {
console.error(`Attempt ${attempt + 1} error:`, error);
}
}
return null;
}
// 批量翻译词组
export async function batchTranslateWithAI(
words: string[],
config?: AIConfig
): Promise<Map<string, string>> {
const results = new Map<string, string>();
for (const word of words) {
try {
const translation = await translateWithAI(word, config);
if (translation) {
results.set(word, translation);
}
} catch (error) {
console.error(`Failed to translate "${word}":`, error);
}
}
return results;
}
+173
View File
@@ -0,0 +1,173 @@
import fs from "fs";
import path from "path";
export interface DBConfig {
host: string;
port: number;
user: string;
password: string;
database: string;
}
export interface AIConfig {
apiKey: string;
baseUrl: string;
model: string;
}
export interface AppConfig {
db: DBConfig;
ai: AIConfig;
}
// 配置文件路径(单一文件)
const configPath = path.join(process.cwd(), "config.json");
/**
* 读取完整配置
*/
function readConfigFile(): Partial<AppConfig> {
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "utf-8");
return JSON.parse(content);
}
} catch (e) {
console.error("Config read error:", e);
}
return {};
}
/**
* 写入完整配置
*/
function writeConfigFile(config: AppConfig): void {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
}
/**
* 获取数据库配置
* 优先级:环境变量 > config.json
*/
export function getDBConfig(): DBConfig {
let host = "127.0.0.1";
let port = 3306;
let user = "root";
let password = "";
let database = "recipe_tools";
// 环境变量优先级最高
if (process.env.DB_HOST) host = process.env.DB_HOST;
if (process.env.DB_PORT) port = parseInt(process.env.DB_PORT);
if (process.env.DB_USER) user = process.env.DB_USER;
if (process.env.DB_PASSWORD) password = process.env.DB_PASSWORD;
if (process.env.DB_NAME) database = process.env.DB_NAME;
// config.json 次之(仅当环境变量未设置时)
const fileConfig = readConfigFile();
if (fileConfig.db) {
if (fileConfig.db.host && !process.env.DB_HOST) host = fileConfig.db.host;
if (fileConfig.db.port && !process.env.DB_PORT) port = fileConfig.db.port;
if (fileConfig.db.user && !process.env.DB_USER) user = fileConfig.db.user;
if (fileConfig.db.password && !process.env.DB_PASSWORD) password = fileConfig.db.password;
if (fileConfig.db.database && !process.env.DB_NAME) database = fileConfig.db.database;
}
return { host, port, user, password, database };
}
/**
* 获取AI翻译配置
* 优先级:环境变量 > config.json
*/
export function getAIConfig(): AIConfig {
let apiKey = "";
let baseUrl = "https://api.openai.com/v1";
let model = "gpt-3.5-turbo";
// 环境变量优先级最高
if (process.env.AI_API_KEY) apiKey = process.env.AI_API_KEY;
if (process.env.AI_BASE_URL) baseUrl = process.env.AI_BASE_URL;
if (process.env.AI_MODEL) model = process.env.AI_MODEL;
// config.json 次之(仅当环境变量未设置时)
const fileConfig = readConfigFile();
if (fileConfig.ai) {
if (fileConfig.ai.apiKey && !process.env.AI_API_KEY) apiKey = fileConfig.ai.apiKey;
if (fileConfig.ai.baseUrl && !process.env.AI_BASE_URL) baseUrl = fileConfig.ai.baseUrl;
if (fileConfig.ai.model && !process.env.AI_MODEL) model = fileConfig.ai.model;
}
return { apiKey, baseUrl, model };
}
/**
* 获取完整配置(兼容旧接口)
*/
export function getConfig(): AppConfig {
return {
db: getDBConfig(),
ai: getAIConfig(),
};
}
/**
* 保存数据库配置
*/
export function saveDBConfig(config: Partial<DBConfig>): DBConfig {
const current = getDBConfig();
const newConfig: DBConfig = {
host: config.host ?? current.host,
port: config.port ?? current.port,
user: config.user ?? current.user,
password: config.password ?? current.password,
database: config.database ?? current.database,
};
// 读取现有配置,更新 db 部分
const fileConfig = readConfigFile();
const updatedConfig: AppConfig = {
ai: fileConfig.ai ?? { apiKey: "", baseUrl: "https://api.openai.com/v1", model: "gpt-3.5-turbo" },
db: newConfig,
};
writeConfigFile(updatedConfig);
return newConfig;
}
/**
* 保存AI翻译配置
*/
export function saveAIConfig(config: Partial<AIConfig>): AIConfig {
const current = getAIConfig();
const newConfig: AIConfig = {
apiKey: config.apiKey ?? current.apiKey,
baseUrl: config.baseUrl ?? current.baseUrl,
model: config.model ?? current.model,
};
// 读取现有配置,更新 ai 部分
const fileConfig = readConfigFile();
const updatedConfig: AppConfig = {
db: fileConfig.db ?? { host: "127.0.0.1", port: 3306, user: "root", password: "", database: "recipe_tools" },
ai: newConfig,
};
writeConfigFile(updatedConfig);
return newConfig;
}
/**
* 保存完整配置(兼容旧接口)
*/
export function saveConfig(config: Partial<AppConfig>): AppConfig {
const current = getConfig();
const fileConfig = readConfigFile();
return {
db: config.db ? saveDBConfig(config.db) : current.db,
ai: config.ai ? saveAIConfig(config.ai) : current.ai,
};
}
+67
View File
@@ -0,0 +1,67 @@
import mysql from "mysql2/promise";
const pool = mysql.createPool({
host: process.env.DB_HOST || "127.0.0.1",
port: parseInt(process.env.DB_PORT || "3306"),
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD || "",
database: process.env.DB_NAME || "recipe_tools",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
export default pool;
/**
* 根据中文菜名查询英文翻译
* @param chineseName 中文菜名
* @returns 翻译结果或 null
*/
export async function getTranslation(chineseName: string): Promise<string | null> {
try {
const [rows] = await pool.execute<mysql.RowDataPacket[]>(
"SELECT english FROM words WHERE word = ? LIMIT 1",
[chineseName]
);
if (rows.length > 0 && rows[0].english) {
return rows[0].english as string;
}
return null;
} catch (error) {
console.error("Database query error:", error);
return null;
}
}
/**
* 批量查询翻译
* @param chineseNames 中文菜名数组
* @returns 翻译结果映射 { chineseName: englishName }
*/
export async function getTranslations(
chineseNames: string[]
): Promise<Map<string, string>> {
const result = new Map<string, string>();
if (chineseNames.length === 0) return result;
try {
const placeholders = chineseNames.map(() => "?").join(",");
const [rows] = await pool.execute<mysql.RowDataPacket[]>(
`SELECT word, english FROM words WHERE word IN (${placeholders})`,
chineseNames
);
for (const row of rows) {
if (row.word && row.english) {
result.set(row.word as string, row.english as string);
}
}
} catch (error) {
console.error("Database batch query error:", error);
}
return result;
}