This commit is contained in:
2026-02-02 23:32:39 +08:00
parent fa6301538b
commit 76749549ff
18 changed files with 590 additions and 74 deletions

View File

@@ -1,2 +1,2 @@
export * from './translator-action';
export * from './translator-action-dto';
export * from './translator-action-dto';

View File

@@ -1,40 +1,27 @@
import { TSharedTranslationResult } from "@/shared";
import {
LENGTH_MAX_LANGUAGE,
LENGTH_MIN_LANGUAGE,
LENGTH_MAX_TRANSLATOR_TEXT,
LENGTH_MIN_TRANSLATOR_TEXT,
} from "@/shared/constant";
import { generateValidator } from "@/utils/validate";
import z from "zod";
export interface CreateTranslationHistoryInput {
userId?: string;
sourceText: string;
sourceLanguage: string;
targetLanguage: string;
translatedText: string;
sourceIpa?: string;
targetIpa?: string;
}
const schemaActionInputTranslateText = z.object({
sourceText: z.string().min(LENGTH_MIN_TRANSLATOR_TEXT).max(LENGTH_MAX_TRANSLATOR_TEXT),
targetLanguage: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE),
forceRetranslate: z.boolean().optional().default(false),
needIpa: z.boolean().optional().default(true),
userId: z.string().optional(),
});
export interface TranslationHistoryQuery {
sourceText: string;
targetLanguage: string;
}
export type ActionInputTranslateText = z.infer<typeof schemaActionInputTranslateText>;
export interface TranslateTextInput {
sourceText: string;
targetLanguage: string;
forceRetranslate?: boolean; // 默认 false
needIpa?: boolean; // 默认 true
userId?: string; // 可选用户 ID
}
export const validateActionInputTranslateText = generateValidator(schemaActionInputTranslateText);
export interface TranslateTextOutput {
sourceText: string;
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa: string; // 如果 needIpa=false返回空字符串
targetIpa: string; // 如果 needIpa=false返回空字符串
}
export interface TranslationLLMResponse {
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa?: string; // 可选,根据 needIpa 决定
targetIpa?: string; // 可选,根据 needIpa 决定
}
export type ActionOutputTranslateText = {
message: string;
success: boolean;
data?: TSharedTranslationResult;
};

View File

@@ -1 +1,103 @@
"use server";
import {
ActionInputTranslateText,
ActionOutputTranslateText,
validateActionInputTranslateText,
} from "./translator-action-dto";
import { ValidateError } from "@/lib/errors";
import { serviceTranslateText } from "./translator-service";
import { getAnswer } from "@/lib/bigmodel/zhipu";
export const actionTranslateText = async (
dto: ActionInputTranslateText
): Promise<ActionOutputTranslateText> => {
try {
return {
message: "success",
success: true,
data: await serviceTranslateText(validateActionInputTranslateText(dto)),
};
} catch (e) {
if (e instanceof ValidateError) {
return {
success: false,
message: e.message,
};
}
console.log(e);
return {
success: false,
message: "Unknown error occurred.",
};
}
};
/**
* @deprecated 保留此函数以支持旧代码text-speaker 功能)
*/
export const genIPA = async (text: string) => {
return (
"[" +
(
await getAnswer(
`
<text>${text}</text>
请生成以上文本的严式国际音标
然后直接发给我
不要附带任何说明
不要擅自增减符号
不许用"/"或者"[]"包裹
`.trim(),
)
)
.replaceAll("[", "")
.replaceAll("]", "") +
"]"
);
};
/**
* @deprecated 保留此函数以支持旧代码text-speaker 功能)
*/
export const genLanguage = async (text: string) => {
const language = await getAnswer([
{
role: "system",
content: `
你是一个语言检测工具。请识别文本的语言并返回语言名称。
返回语言的标准英文名称,例如:
- 中文: Chinese
- 英语: English
- 日语: Japanese
- 韩语: Korean
- 法语: French
- 德语: German
- 意大利语: Italian
- 葡萄牙语: Portuguese
- 西班牙语: Spanish
- 俄语: Russian
- 阿拉伯语: Arabic
- 印地语: Hindi
- 泰语: Thai
- 越南语: Vietnamese
- 等等...
如果无法识别语言,返回 "Unknown"
规则:
1. 只返回语言的标准英文名称
2. 首字母大写,其余小写
3. 不要附带任何说明
4. 不要擅自增减符号
`.trim()
},
{
role: "user",
content: `<text>${text}</text>`
}
]);
return language.trim();
};

View File

@@ -0,0 +1,23 @@
export type RepoInputSelectLatestTranslation = {
sourceText: string;
targetLanguage: string;
};
export type RepoOutputSelectLatestTranslation = {
id: number;
translatedText: string;
sourceLanguage: string;
targetLanguage: string;
sourceIpa: string | null;
targetIpa: string | null;
} | null;
export type RepoInputCreateTranslationHistory = {
userId?: string;
sourceText: string;
sourceLanguage: string;
targetLanguage: string;
translatedText: string;
sourceIpa?: string;
targetIpa?: string;
};

View File

@@ -1,31 +1,41 @@
"use server";
import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./translator-action-dto";
import {
RepoInputCreateTranslationHistory,
RepoInputSelectLatestTranslation,
RepoOutputSelectLatestTranslation,
} from "./translator-repository-dto";
import prisma from "@/lib/db";
/**
* 创建翻译历史记录
*/
export async function repoCreateTranslationHistory(data: CreateTranslationHistoryInput) {
return prisma.translationHistory.create({
data: data,
});
export async function repoSelectLatestTranslation(
dto: RepoInputSelectLatestTranslation
): Promise<RepoOutputSelectLatestTranslation> {
const result = await prisma.translationHistory.findFirst({
where: {
sourceText: dto.sourceText,
targetLanguage: dto.targetLanguage,
},
orderBy: {
createdAt: "desc",
},
});
if (!result) {
return null;
}
return {
id: result.id,
translatedText: result.translatedText,
sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage,
sourceIpa: result.sourceIpa,
targetIpa: result.targetIpa,
};
}
/**
* 查询最新的翻译记录
* @param sourceText 源文本
* @param targetLanguage 目标语言
* @returns 最新的翻译记录,如果不存在则返回 null
*/
export async function repoSelectLatestTranslation(query: TranslationHistoryQuery) {
return prisma.translationHistory.findFirst({
where: {
sourceText: query.sourceText,
targetLanguage: query.targetLanguage,
},
orderBy: {
createdAt: 'desc',
},
});
export async function repoCreateTranslationHistory(
data: RepoInputCreateTranslationHistory
) {
return await prisma.translationHistory.create({
data: data,
});
}

View File

@@ -0,0 +1,11 @@
import { TSharedTranslationResult } from "@/shared";
export type ServiceInputTranslateText = {
sourceText: string;
targetLanguage: string;
forceRetranslate: boolean;
needIpa: boolean;
userId?: string;
};
export type ServiceOutputTranslateText = TSharedTranslationResult;

View File

@@ -0,0 +1,69 @@
import { executeTranslation } from "@/lib/bigmodel/translator";
import { repoCreateTranslationHistory, repoSelectLatestTranslation } from "./translator-repository";
import { ServiceInputTranslateText, ServiceOutputTranslateText } from "./translator-service-dto";
export const serviceTranslateText = async (
dto: ServiceInputTranslateText
): Promise<ServiceOutputTranslateText> => {
const { sourceText, targetLanguage, forceRetranslate, needIpa, userId } = dto;
// Check for existing translation
const lastTranslation = await repoSelectLatestTranslation({
sourceText,
targetLanguage,
});
if (forceRetranslate || !lastTranslation) {
// Call AI for translation
const response = await executeTranslation(
sourceText,
targetLanguage,
needIpa
);
// Save translation history asynchronously (don't block response)
repoCreateTranslationHistory({
userId,
sourceText,
sourceLanguage: response.sourceLanguage,
targetLanguage: response.targetLanguage,
translatedText: response.translatedText,
sourceIpa: needIpa ? response.sourceIpa : undefined,
targetIpa: needIpa ? response.targetIpa : undefined,
}).catch((error) => {
console.error("Failed to save translation data:", error);
});
return {
sourceText: response.sourceText,
translatedText: response.translatedText,
sourceLanguage: response.sourceLanguage,
targetLanguage: response.targetLanguage,
sourceIpa: response.sourceIpa || "",
targetIpa: response.targetIpa || "",
};
} else {
// Return cached translation
// Still save a history record for analytics
repoCreateTranslationHistory({
userId,
sourceText,
sourceLanguage: lastTranslation.sourceLanguage,
targetLanguage: lastTranslation.targetLanguage,
translatedText: lastTranslation.translatedText,
sourceIpa: lastTranslation.sourceIpa || undefined,
targetIpa: lastTranslation.targetIpa || undefined,
}).catch((error) => {
console.error("Failed to save translation data:", error);
});
return {
sourceText,
translatedText: lastTranslation.translatedText,
sourceLanguage: lastTranslation.sourceLanguage,
targetLanguage: lastTranslation.targetLanguage,
sourceIpa: lastTranslation.sourceIpa || "",
targetIpa: lastTranslation.targetIpa || "",
};
}
};