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:
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user