diff --git a/messages/de-DE.json b/messages/de-DE.json
index 4a36f53..2fed8f0 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -166,6 +166,9 @@
"resetPasswordFailed": "Reset-E-Mail konnte nicht gesendet werden",
"resetPasswordEmailSent": "Reset-E-Mail erfolgreich gesendet",
"resetPasswordEmailSentHint": "Wir haben einen Link zum Zurücksetzen Ihres Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang.",
+ "verifyYourEmail": "E-Mail bestätigen",
+ "verificationEmailSent": "Bestätigungs-E-Mail gesendet",
+ "verificationEmailSentHint": "Wir haben eine Bestätigungs-E-Mail an {email} gesendet. Bitte klicken Sie auf den Link in der E-Mail, um Ihr Konto zu bestätigen.",
"checkYourEmail": "Überprüfen Sie Ihre E-Mail",
"backToLogin": "Zurück zur Anmeldung",
"resetPassword": "Passwort zurücksetzen",
diff --git a/messages/en-US.json b/messages/en-US.json
index 141b153..9bef781 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -157,6 +157,9 @@
"resetPasswordFailed": "Failed to send reset email",
"resetPasswordEmailSent": "Reset email sent successfully",
"resetPasswordEmailSentHint": "We've sent a password reset link to your email address. Please check your inbox.",
+ "verifyYourEmail": "Verify Your Email",
+ "verificationEmailSent": "Verification email sent",
+ "verificationEmailSentHint": "We've sent a verification email to {email}. Please click the link in the email to verify your account.",
"checkYourEmail": "Check Your Email",
"backToLogin": "Back to Login",
"resetPassword": "Reset Password",
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index c2434e2..346c005 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -166,6 +166,9 @@
"resetPasswordFailed": "Échec de l'envoi de l'e-mail de réinitialisation",
"resetPasswordEmailSent": "E-mail de réinitialisation envoyé avec succès",
"resetPasswordEmailSentHint": "Nous avons envoyé un lien de réinitialisation de mot de passe à votre adresse e-mail. Veuillez vérifier votre boîte de réception.",
+ "verifyYourEmail": "Vérifier votre e-mail",
+ "verificationEmailSent": "E-mail de vérification envoyé",
+ "verificationEmailSentHint": "Nous avons envoyé un e-mail de vérification à {email}. Veuillez cliquer sur le lien dans l'e-mail pour vérifier votre compte.",
"checkYourEmail": "Vérifiez votre e-mail",
"backToLogin": "Retour à la connexion",
"resetPassword": "Réinitialiser le mot de passe",
diff --git a/messages/it-IT.json b/messages/it-IT.json
index 7128995..6846b74 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -166,6 +166,9 @@
"resetPasswordFailed": "Impossibile inviare email di reset",
"resetPasswordEmailSent": "Email di reset inviata con successo",
"resetPasswordEmailSentHint": "Abbiamo inviato un link per reimpostare la password al tuo indirizzo email. Controlla la tua casella di posta.",
+ "verifyYourEmail": "Verifica la tua Email",
+ "verificationEmailSent": "Email di verifica inviata",
+ "verificationEmailSentHint": "Abbiamo inviato un'email di verifica a {email}. Clicca sul link nell'email per verificare il tuo account.",
"checkYourEmail": "Controlla la tua Email",
"backToLogin": "Torna al Login",
"resetPassword": "Reimposta Password",
diff --git a/messages/ja-JP.json b/messages/ja-JP.json
index 7dd24b5..c783e7a 100644
--- a/messages/ja-JP.json
+++ b/messages/ja-JP.json
@@ -157,6 +157,9 @@
"resetPasswordFailed": "リセットメールの送信に失敗しました",
"resetPasswordEmailSent": "リセットメールを送信しました",
"resetPasswordEmailSentHint": "パスワードリセット用のリンクをメールでお送りしました。受信トレイをご確認ください。",
+ "verifyYourEmail": "メールアドレスを確認",
+ "verificationEmailSent": "確認メールを送信しました",
+ "verificationEmailSentHint": "{email} に確認メールを送信しました。メール内のリンクをクリックしてアカウントを確認してください。",
"checkYourEmail": "メールをご確認ください",
"backToLogin": "ログインに戻る",
"resetPassword": "パスワードをリセット",
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index f7e127b..e49a313 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -166,6 +166,9 @@
"resetPasswordFailed": "재설정 이메일 전송 실패",
"resetPasswordEmailSent": "재설정 이메일이 전송되었습니다",
"resetPasswordEmailSentHint": "비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함을 확인해주세요.",
+ "verifyYourEmail": "이메일 인증",
+ "verificationEmailSent": "인증 이메일이 전송되었습니다",
+ "verificationEmailSentHint": "{email}로 인증 이메일을 보냈습니다. 이메일의 링크를 클릭하여 계정을 인증해주세요.",
"checkYourEmail": "이메일을 확인하세요",
"backToLogin": "로그인으로 돌아가기",
"resetPassword": "비밀번호 재설정",
diff --git a/messages/ug-CN.json b/messages/ug-CN.json
index 521f8c2..57ab141 100644
--- a/messages/ug-CN.json
+++ b/messages/ug-CN.json
@@ -166,6 +166,9 @@
"resetPasswordFailed": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش مەغلۇپ بولدى",
"resetPasswordEmailSent": "ئەسلىگە قايتۇرۇش ئېلخېتى مۇۋەپپەقىيەتلىك ئەۋەتىلدى",
"resetPasswordEmailSentHint": "پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسىنى ئېلخەت ئادرېسىڭىزغا ئەۋەتتۇق. ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ.",
+ "verifyYourEmail": "ئېلخەتنى دەلىللەش",
+ "verificationEmailSent": "دەلىللەش ئېلخېتى ئەۋەتىلدى",
+ "verificationEmailSentHint": "{email} غا دەلىللەش ئېلخېتى ئەۋەتتۇق. ئېلخەتتىكى ئۇلانمىنى چېكىپ ھېساباتىڭىزنى دەلىللەڭ.",
"checkYourEmail": "ئېلخېتىڭىزنى تەكشۈرۈڭ",
"backToLogin": "كىرىشكە قايتىش",
"resetPassword": "پارولنى ئەسلىگە قايتۇرۇش",
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index 2cb21e3..c2c342b 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -157,6 +157,9 @@
"resetPasswordFailed": "发送重置邮件失败",
"resetPasswordEmailSent": "重置邮件已发送",
"resetPasswordEmailSentHint": "我们已向您的邮箱发送了密码重置链接,请查收。",
+ "verifyYourEmail": "验证您的邮箱",
+ "verificationEmailSent": "验证邮件已发送",
+ "verificationEmailSentHint": "我们已向 {email} 发送了验证邮件,请点击邮件中的链接完成验证。",
"checkYourEmail": "请查收邮件",
"backToLogin": "返回登录",
"resetPassword": "重置密码",
diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx
index 2a6397e..5151a8e 100644
--- a/src/app/(auth)/forgot-password/page.tsx
+++ b/src/app/(auth)/forgot-password/page.tsx
@@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
-import { authClient } from "@/lib/auth-client";
import Link from "next/link";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
@@ -9,6 +8,7 @@ import { Card, CardBody } from "@/design-system/base/card";
import { Input } from "@/design-system/base/input";
import { PrimaryButton } from "@/design-system/base/button";
import { VStack } from "@/design-system/layout/stack";
+import { actionRequestPasswordReset } from "@/modules/auth/forgot-password-action";
export default function ForgotPasswordPage() {
const t = useTranslations("auth");
@@ -23,16 +23,13 @@ export default function ForgotPasswordPage() {
}
setLoading(true);
- const { error } = await authClient.requestPasswordReset({
- email,
- redirectTo: "/reset-password",
- });
+ const result = await actionRequestPasswordReset({ email });
- if (error) {
- toast.error(error.message ?? t("resetPasswordFailed"));
+ if (!result.success) {
+ toast.error(result.message);
} else {
setSent(true);
- toast.success(t("resetPasswordEmailSent"));
+ toast.success(result.message);
}
setLoading(false);
};
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
index 9f0f470..081d091 100644
--- a/src/app/(auth)/signup/page.tsx
+++ b/src/app/(auth)/signup/page.tsx
@@ -18,6 +18,7 @@ export default function SignUpPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
+ const [verificationSent, setVerificationSent] = useState(false);
const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirect");
@@ -26,10 +27,10 @@ export default function SignUpPage() {
const router = useRouter();
useEffect(() => {
- if (!isPending && session?.user?.username && !redirectTo) {
+ if (!isPending && session?.user?.username && !redirectTo && !verificationSent) {
router.push("/folders");
}
- }, [session, isPending, router, redirectTo]);
+ }, [session, isPending, router, redirectTo, verificationSent]);
const handleSignUp = async () => {
if (!username || !email || !password) {
@@ -49,12 +50,38 @@ export default function SignUpPage() {
toast.error(error.message ?? t("signUpFailed"));
return;
}
- router.push(redirectTo ?? "/folders");
+ setVerificationSent(true);
+ toast.success(t("verificationEmailSent"));
} finally {
setLoading(false);
}
};
+ if (verificationSent) {
+ return (
+
+
+
+
+
+ {t("verifyYourEmail")}
+
+
+ {t("verificationEmailSentHint", { email })}
+
+
+ {t("backToLogin")}
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/src/modules/auth/auth-action.ts b/src/modules/auth/auth-action.ts
index b491209..02a91f8 100644
--- a/src/modules/auth/auth-action.ts
+++ b/src/modules/auth/auth-action.ts
@@ -5,7 +5,6 @@ import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { ValidateError } from "@/lib/errors";
import { createLogger } from "@/lib/logger";
-import { prisma } from "@/lib/db";
import {
ActionInputGetUserProfileByUsername,
ActionInputSignIn,
@@ -20,7 +19,8 @@ import {
import {
serviceGetUserProfileByUsername,
serviceSignIn,
- serviceSignUp
+ serviceSignUp,
+ serviceDeleteAccount
} from "./auth-service";
// Re-export types for use in components
@@ -194,75 +194,11 @@ export async function actionDeleteAccount(): Promise
return { success: false, message: "Unauthorized" };
}
- const userId = session.user.id;
+ const result = await serviceDeleteAccount({ userId: session.user.id });
- await prisma.$transaction(async (tx) => {
- // Delete in correct order to avoid foreign key constraints
- // 1. Revlogs (depend on cards)
- await tx.revlog.deleteMany({
- where: { card: { note: { userId } } }
- });
-
- // 2. Cards (depend on notes and decks)
- await tx.card.deleteMany({
- where: { note: { userId } }
- });
-
- // 3. Notes (depend on note types)
- await tx.note.deleteMany({
- where: { userId }
- });
-
- // 4. Note types
- await tx.noteType.deleteMany({
- where: { userId }
- });
-
- // 5. Deck favorites
- await tx.deckFavorite.deleteMany({
- where: { userId }
- });
-
- // 6. Decks
- await tx.deck.deleteMany({
- where: { userId }
- });
-
- // 7. Follows (both as follower and following)
- await tx.follow.deleteMany({
- where: {
- OR: [
- { followerId: userId },
- { followingId: userId }
- ]
- }
- });
-
- // 8. Dictionary lookups
- await tx.dictionaryLookUp.deleteMany({
- where: { userId }
- });
-
- // 9. Translation history
- await tx.translationHistory.deleteMany({
- where: { userId }
- });
-
- // 10. Sessions
- await tx.session.deleteMany({
- where: { userId }
- });
-
- // 11. Accounts
- await tx.account.deleteMany({
- where: { userId }
- });
-
- // 12. Finally, delete the user
- await tx.user.delete({
- where: { id: userId }
- });
- });
+ if (!result.success) {
+ return { success: false, message: "Failed to delete account" };
+ }
return { success: true, message: "Account deleted successfully" };
} catch (e) {
diff --git a/src/modules/auth/auth-repository-dto.ts b/src/modules/auth/auth-repository-dto.ts
index 37bdb99..495297e 100644
--- a/src/modules/auth/auth-repository-dto.ts
+++ b/src/modules/auth/auth-repository-dto.ts
@@ -25,3 +25,12 @@ export type RepoInputFindUserById = {
export type RepoInputFindUserByEmail = {
email: string;
};
+
+// Delete user cascade types
+export type RepoInputDeleteUserCascade = {
+ userId: string;
+};
+
+export type RepoOutputDeleteUserCascade = {
+ success: boolean;
+};
diff --git a/src/modules/auth/auth-repository.ts b/src/modules/auth/auth-repository.ts
index 3f9031e..95a7154 100644
--- a/src/modules/auth/auth-repository.ts
+++ b/src/modules/auth/auth-repository.ts
@@ -1,11 +1,16 @@
import { prisma } from "@/lib/db";
+import { createLogger } from "@/lib/logger";
import {
RepoInputFindUserByEmail,
RepoInputFindUserById,
RepoInputFindUserByUsername,
- RepoOutputUserProfile
+ RepoInputDeleteUserCascade,
+ RepoOutputUserProfile,
+ RepoOutputDeleteUserCascade
} from "./auth-repository-dto";
+const log = createLogger("auth-repository");
+
export async function repoFindUserByUsername(dto: RepoInputFindUserByUsername): Promise {
const user = await prisma.user.findUnique({
where: { username: dto.username },
@@ -62,3 +67,68 @@ export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promis
return user;
}
+
+export async function repoDeleteUserCascade(dto: RepoInputDeleteUserCascade): Promise {
+ const { userId } = dto;
+
+ log.info("Starting cascade delete for user", { userId });
+
+ await prisma.$transaction(async (tx) => {
+ await tx.revlog.deleteMany({
+ where: { card: { note: { userId } } }
+ });
+
+ await tx.card.deleteMany({
+ where: { note: { userId } }
+ });
+
+ await tx.note.deleteMany({
+ where: { userId }
+ });
+
+ await tx.noteType.deleteMany({
+ where: { userId }
+ });
+
+ await tx.deckFavorite.deleteMany({
+ where: { userId }
+ });
+
+ await tx.deck.deleteMany({
+ where: { userId }
+ });
+
+ await tx.follow.deleteMany({
+ where: {
+ OR: [
+ { followerId: userId },
+ { followingId: userId }
+ ]
+ }
+ });
+
+ await tx.dictionaryLookUp.deleteMany({
+ where: { userId }
+ });
+
+ await tx.translationHistory.deleteMany({
+ where: { userId }
+ });
+
+ await tx.session.deleteMany({
+ where: { userId }
+ });
+
+ await tx.account.deleteMany({
+ where: { userId }
+ });
+
+ await tx.user.delete({
+ where: { id: userId }
+ });
+ });
+
+ log.info("Cascade delete completed for user", { userId });
+
+ return { success: true };
+}
diff --git a/src/modules/auth/auth-service-dto.ts b/src/modules/auth/auth-service-dto.ts
index 6159b94..543a4f8 100644
--- a/src/modules/auth/auth-service-dto.ts
+++ b/src/modules/auth/auth-service-dto.ts
@@ -38,3 +38,11 @@ export type ServiceOutputUserProfile = {
createdAt: Date;
updatedAt: Date;
} | null;
+
+export type ServiceInputDeleteAccount = {
+ userId: string;
+};
+
+export type ServiceOutputDeleteAccount = {
+ success: boolean;
+};
diff --git a/src/modules/auth/auth-service.ts b/src/modules/auth/auth-service.ts
index c971e3d..cc23151 100644
--- a/src/modules/auth/auth-service.ts
+++ b/src/modules/auth/auth-service.ts
@@ -1,15 +1,18 @@
import { auth } from "@/auth";
import {
repoFindUserByUsername,
- repoFindUserById
+ repoFindUserById,
+ repoDeleteUserCascade
} from "./auth-repository";
import {
ServiceInputGetUserProfileByUsername,
ServiceInputGetUserProfileById,
ServiceInputSignIn,
ServiceInputSignUp,
+ ServiceInputDeleteAccount,
ServiceOutputAuth,
- ServiceOutputUserProfile
+ ServiceOutputUserProfile,
+ ServiceOutputDeleteAccount
} from "./auth-service-dto";
/**
@@ -92,3 +95,7 @@ export async function serviceGetUserProfileByUsername(dto: ServiceInputGetUserPr
export async function serviceGetUserProfileById(dto: ServiceInputGetUserProfileById): Promise {
return await repoFindUserById(dto);
}
+
+export async function serviceDeleteAccount(dto: ServiceInputDeleteAccount): Promise {
+ return await repoDeleteUserCascade({ userId: dto.userId });
+}
diff --git a/src/modules/auth/forgot-password-action-dto.ts b/src/modules/auth/forgot-password-action-dto.ts
new file mode 100644
index 0000000..9328a47
--- /dev/null
+++ b/src/modules/auth/forgot-password-action-dto.ts
@@ -0,0 +1,12 @@
+import { z } from "zod";
+
+export const schemaActionInputForgotPassword = z.object({
+ email: z.string().email("请输入有效的邮箱地址"),
+});
+
+export type ActionInputForgotPassword = z.infer;
+
+export interface ActionOutputForgotPassword {
+ success: boolean;
+ message: string;
+}
diff --git a/src/modules/auth/forgot-password-action.ts b/src/modules/auth/forgot-password-action.ts
new file mode 100644
index 0000000..6c9ee8f
--- /dev/null
+++ b/src/modules/auth/forgot-password-action.ts
@@ -0,0 +1,35 @@
+"use server";
+
+import { createLogger } from "@/lib/logger";
+import { validate } from "@/utils/validate";
+import { ValidateError } from "@/lib/errors";
+import {
+ schemaActionInputForgotPassword,
+ type ActionInputForgotPassword,
+ type ActionOutputForgotPassword,
+} from "./forgot-password-action-dto";
+import { serviceRequestPasswordReset } from "./forgot-password-service";
+
+const log = createLogger("forgot-password-action");
+
+export async function actionRequestPasswordReset(
+ input: unknown
+): Promise {
+ try {
+ const dto = validate(input, schemaActionInputForgotPassword) as ActionInputForgotPassword;
+
+ return await serviceRequestPasswordReset({ email: dto.email });
+ } catch (e) {
+ if (e instanceof ValidateError) {
+ return {
+ success: false,
+ message: e.message,
+ };
+ }
+ log.error("Password reset request failed", { error: e });
+ return {
+ success: false,
+ message: "发送重置邮件失败,请稍后重试",
+ };
+ }
+}
diff --git a/src/modules/auth/forgot-password-repository-dto.ts b/src/modules/auth/forgot-password-repository-dto.ts
new file mode 100644
index 0000000..27098a6
--- /dev/null
+++ b/src/modules/auth/forgot-password-repository-dto.ts
@@ -0,0 +1,7 @@
+export type RepoInputFindUserByEmail = {
+ email: string;
+};
+
+export type RepoOutputFindUserByEmail = {
+ id: string;
+} | null;
diff --git a/src/modules/auth/forgot-password-repository.ts b/src/modules/auth/forgot-password-repository.ts
new file mode 100644
index 0000000..6241e11
--- /dev/null
+++ b/src/modules/auth/forgot-password-repository.ts
@@ -0,0 +1,19 @@
+import { prisma } from "@/lib/db";
+import { createLogger } from "@/lib/logger";
+import {
+ RepoInputFindUserByEmail,
+ RepoOutputFindUserByEmail
+} from "./forgot-password-repository-dto";
+
+const log = createLogger("forgot-password-repository");
+
+export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promise {
+ log.debug("Finding user by email", { email: dto.email });
+
+ const user = await prisma.user.findUnique({
+ where: { email: dto.email },
+ select: { id: true },
+ });
+
+ return user;
+}
diff --git a/src/modules/auth/forgot-password-service-dto.ts b/src/modules/auth/forgot-password-service-dto.ts
new file mode 100644
index 0000000..4d198c0
--- /dev/null
+++ b/src/modules/auth/forgot-password-service-dto.ts
@@ -0,0 +1,8 @@
+export type ServiceInputRequestPasswordReset = {
+ email: string;
+};
+
+export type ServiceOutputRequestPasswordReset = {
+ success: boolean;
+ message: string;
+};
diff --git a/src/modules/auth/forgot-password-service.ts b/src/modules/auth/forgot-password-service.ts
new file mode 100644
index 0000000..948bb1f
--- /dev/null
+++ b/src/modules/auth/forgot-password-service.ts
@@ -0,0 +1,34 @@
+import { auth } from "@/auth";
+import { createLogger } from "@/lib/logger";
+import { repoFindUserByEmail } from "./forgot-password-repository";
+import {
+ ServiceInputRequestPasswordReset,
+ ServiceOutputRequestPasswordReset
+} from "./forgot-password-service-dto";
+
+const log = createLogger("forgot-password-service");
+
+export async function serviceRequestPasswordReset(dto: ServiceInputRequestPasswordReset): Promise {
+ log.info("Processing password reset request", { email: dto.email });
+
+ const user = await repoFindUserByEmail({ email: dto.email });
+
+ if (!user) {
+ return {
+ success: false,
+ message: "该邮箱未注册",
+ };
+ }
+
+ await auth.api.requestPasswordReset({
+ body: {
+ email: dto.email,
+ redirectTo: "/reset-password",
+ },
+ });
+
+ return {
+ success: true,
+ message: "重置密码邮件已发送,请检查您的邮箱",
+ };
+}
diff --git a/src/modules/card/card-action.ts b/src/modules/card/card-action.ts
index 6fcd9e0..faef72c 100644
--- a/src/modules/card/card-action.ts
+++ b/src/modules/card/card-action.ts
@@ -42,8 +42,8 @@ import {
serviceGetCardStats,
serviceDeleteCard,
serviceGetCardByIdWithNote,
+ serviceCheckCardOwnership,
} from "./card-service";
-import { repoGetCardDeckOwnerId } from "./card-repository";
import { CardQueue } from "../../../generated/prisma/enums";
const log = createLogger("card-action");
@@ -161,8 +161,7 @@ async function checkCardOwnership(cardId: bigint): Promise {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) return false;
- const ownerId = await repoGetCardDeckOwnerId(cardId);
- return ownerId === session.user.id;
+ return serviceCheckCardOwnership({ cardId, userId: session.user.id });
}
async function getCurrentUserId(): Promise {
diff --git a/src/modules/card/card-service-dto.ts b/src/modules/card/card-service-dto.ts
index 78f76bc..d3ac33b 100644
--- a/src/modules/card/card-service-dto.ts
+++ b/src/modules/card/card-service-dto.ts
@@ -34,6 +34,13 @@ export interface ServiceInputGetCardStats {
deckId: number;
}
+export interface ServiceInputCheckCardOwnership {
+ cardId: bigint;
+ userId: string;
+}
+
+export type ServiceOutputCheckCardOwnership = boolean;
+
export type ServiceOutputCard = {
id: bigint;
noteId: bigint;
@@ -94,11 +101,17 @@ export type ServiceOutputReviewResult = {
export const SM2_CONFIG = {
LEARNING_STEPS: [1, 10],
+ RELEARNING_STEPS: [10],
GRADUATING_INTERVAL_GOOD: 1,
GRADUATING_INTERVAL_EASY: 4,
EASY_INTERVAL: 4,
MINIMUM_FACTOR: 1300,
DEFAULT_FACTOR: 2500,
+ MAXIMUM_INTERVAL: 36500,
+ EASY_BONUS: 1.3,
+ HARD_INTERVAL: 1.2,
+ NEW_INTERVAL: 0.0,
+ INTERVAL_MODIFIER: 1.0,
FACTOR_ADJUSTMENTS: {
1: -200,
2: -150,
diff --git a/src/modules/card/card-service.ts b/src/modules/card/card-service.ts
index 834100f..3792cfa 100644
--- a/src/modules/card/card-service.ts
+++ b/src/modules/card/card-service.ts
@@ -11,6 +11,7 @@ import {
repoGetCardStats,
repoDeleteCard,
repoGetCardsByNoteId,
+ repoGetCardDeckOwnerId,
} from "./card-repository";
import {
RepoInputUpdateCard,
@@ -23,11 +24,13 @@ import {
ServiceInputGetNewCards,
ServiceInputGetCardsByDeckId,
ServiceInputGetCardStats,
+ ServiceInputCheckCardOwnership,
ServiceOutputCard,
ServiceOutputCardWithNote,
ServiceOutputCardStats,
ServiceOutputScheduledCard,
ServiceOutputReviewResult,
+ ServiceOutputCheckCardOwnership,
ReviewEase,
SM2_CONFIG,
} from "./card-service-dto";
@@ -50,7 +53,11 @@ function calculateNextReviewTime(intervalDays: number): Date {
return new Date(now + intervalDays * 86400 * 1000);
}
-function scheduleNewCard(ease: ReviewEase, factor: number): {
+function clampInterval(interval: number): number {
+ return Math.min(Math.max(1, interval), SM2_CONFIG.MAXIMUM_INTERVAL);
+}
+
+function scheduleNewCard(ease: ReviewEase, currentFactor: number): {
type: CardType;
queue: CardQueue;
ivl: number;
@@ -63,21 +70,53 @@ function scheduleNewCard(ease: ReviewEase, factor: number): {
queue: CardQueue.LEARNING,
ivl: 0,
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
- newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]),
+ newFactor: currentFactor,
};
}
- const ivl = SM2_CONFIG.INITIAL_INTERVALS[ease];
+ if (ease === 2) {
+ if (SM2_CONFIG.LEARNING_STEPS.length >= 2) {
+ const avgStep = (SM2_CONFIG.LEARNING_STEPS[0] + SM2_CONFIG.LEARNING_STEPS[1]) / 2;
+ return {
+ type: CardType.LEARNING,
+ queue: CardQueue.LEARNING,
+ ivl: 0,
+ due: Math.floor(Date.now() / 1000) + avgStep * 60,
+ newFactor: currentFactor,
+ };
+ }
+ return {
+ type: CardType.LEARNING,
+ queue: CardQueue.LEARNING,
+ ivl: 0,
+ due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
+ newFactor: currentFactor,
+ };
+ }
+
+ if (ease === 3) {
+ return {
+ type: CardType.LEARNING,
+ queue: CardQueue.LEARNING,
+ ivl: 0,
+ due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
+ newFactor: currentFactor,
+ };
+ }
+
+ const ivl = SM2_CONFIG.EASY_INTERVAL;
+ const newFactor = SM2_CONFIG.DEFAULT_FACTOR + SM2_CONFIG.FACTOR_ADJUSTMENTS[4];
+
return {
type: CardType.REVIEW,
queue: CardQueue.REVIEW,
ivl,
due: calculateDueDate(ivl),
- newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[ease]),
+ newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, newFactor),
};
}
-function scheduleLearningCard(ease: ReviewEase, factor: number, left: number): {
+function scheduleLearningCard(ease: ReviewEase, currentFactor: number, left: number): {
type: CardType;
queue: CardQueue;
ivl: number;
@@ -85,45 +124,88 @@ function scheduleLearningCard(ease: ReviewEase, factor: number, left: number): {
newFactor: number;
newLeft: number;
} {
+ const steps = SM2_CONFIG.LEARNING_STEPS;
+ const totalSteps = steps.length;
+
if (ease === 1) {
return {
type: CardType.LEARNING,
queue: CardQueue.LEARNING,
ivl: 0,
- due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
- newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]),
- newLeft: SM2_CONFIG.LEARNING_STEPS.length * 1000 + SM2_CONFIG.LEARNING_STEPS.length,
+ due: Math.floor(Date.now() / 1000) + steps[0] * 60,
+ newFactor: currentFactor,
+ newLeft: totalSteps * 1000,
};
}
const stepIndex = Math.floor(left % 1000);
- if (ease === 2 && stepIndex < SM2_CONFIG.LEARNING_STEPS.length - 1) {
- const nextStep = stepIndex + 1;
+
+ if (ease === 2) {
+ if (stepIndex === 0 && steps.length >= 2) {
+ const avgStep = (steps[0] + steps[1]) / 2;
+ return {
+ type: CardType.LEARNING,
+ queue: CardQueue.LEARNING,
+ ivl: 0,
+ due: Math.floor(Date.now() / 1000) + avgStep * 60,
+ newFactor: currentFactor,
+ newLeft: left,
+ };
+ }
+ if (stepIndex < steps.length - 1) {
+ const nextStep = stepIndex + 1;
+ return {
+ type: CardType.LEARNING,
+ queue: CardQueue.LEARNING,
+ ivl: 0,
+ due: Math.floor(Date.now() / 1000) + steps[nextStep] * 60,
+ newFactor: currentFactor,
+ newLeft: nextStep * 1000 + (totalSteps - nextStep),
+ };
+ }
+ }
+
+ if (ease === 3) {
+ if (stepIndex < steps.length - 1) {
+ const nextStep = stepIndex + 1;
+ return {
+ type: CardType.LEARNING,
+ queue: CardQueue.LEARNING,
+ ivl: 0,
+ due: Math.floor(Date.now() / 1000) + steps[nextStep] * 60,
+ newFactor: currentFactor,
+ newLeft: nextStep * 1000 + (totalSteps - nextStep),
+ };
+ }
+
+ const ivl = SM2_CONFIG.GRADUATING_INTERVAL_GOOD;
return {
- type: CardType.LEARNING,
- queue: CardQueue.LEARNING,
- ivl: 0,
- due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[nextStep] * 60,
- newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[2]),
- newLeft: nextStep * 1000 + (SM2_CONFIG.LEARNING_STEPS.length - nextStep),
+ type: CardType.REVIEW,
+ queue: CardQueue.REVIEW,
+ ivl,
+ due: calculateDueDate(ivl),
+ newFactor: SM2_CONFIG.DEFAULT_FACTOR,
+ newLeft: 0,
};
}
- const ivl = ease === 4 ? SM2_CONFIG.GRADUATING_INTERVAL_EASY : SM2_CONFIG.GRADUATING_INTERVAL_GOOD;
+ const ivl = SM2_CONFIG.GRADUATING_INTERVAL_EASY;
+ const newFactor = SM2_CONFIG.DEFAULT_FACTOR + SM2_CONFIG.FACTOR_ADJUSTMENTS[4];
+
return {
type: CardType.REVIEW,
queue: CardQueue.REVIEW,
ivl,
due: calculateDueDate(ivl),
- newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[ease]),
+ newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, newFactor),
newLeft: 0,
};
}
function scheduleReviewCard(
ease: ReviewEase,
- ivl: number,
- factor: number,
+ currentIvl: number,
+ currentFactor: number,
lapses: number,
): {
type: CardType;
@@ -134,27 +216,34 @@ function scheduleReviewCard(
newLapses: number;
} {
if (ease === 1) {
+ const newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, currentFactor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]);
+ const newIvl = Math.max(1, Math.floor(currentIvl * SM2_CONFIG.NEW_INTERVAL));
return {
type: CardType.RELEARNING,
queue: CardQueue.LEARNING,
- ivl: 0,
- due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
- newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]),
+ ivl: newIvl,
+ due: Math.floor(Date.now() / 1000) + SM2_CONFIG.RELEARNING_STEPS[0] * 60,
+ newFactor,
newLapses: lapses + 1,
};
}
- const newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[ease]);
- const factorMultiplier = newFactor / 1000;
- let newIvl = Math.floor(ivl * factorMultiplier);
+ let newFactor: number;
+ let newIvl: number;
if (ease === 2) {
- newIvl = Math.max(1, Math.floor(newIvl * 1.2));
- } else if (ease === 4) {
- newIvl = Math.floor(newIvl * 1.3);
+ newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, currentFactor + SM2_CONFIG.FACTOR_ADJUSTMENTS[2]);
+ newIvl = Math.floor(currentIvl * SM2_CONFIG.HARD_INTERVAL * SM2_CONFIG.INTERVAL_MODIFIER);
+ } else if (ease === 3) {
+ newFactor = currentFactor;
+ newIvl = Math.floor(currentIvl * (currentFactor / 1000) * SM2_CONFIG.INTERVAL_MODIFIER);
+ } else {
+ newIvl = Math.floor(currentIvl * (currentFactor / 1000) * SM2_CONFIG.EASY_BONUS * SM2_CONFIG.INTERVAL_MODIFIER);
+ newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, currentFactor + SM2_CONFIG.FACTOR_ADJUSTMENTS[4]);
}
- newIvl = Math.max(1, newIvl);
+ newIvl = clampInterval(newIvl);
+ newIvl = Math.max(currentIvl + 1, newIvl);
return {
type: CardType.REVIEW,
@@ -237,7 +326,9 @@ export async function serviceAnswerCard(
due: result.due,
factor: result.newFactor,
reps: card.reps + 1,
- left: result.type === CardType.LEARNING ? SM2_CONFIG.LEARNING_STEPS.length * 1000 + SM2_CONFIG.LEARNING_STEPS.length : 0,
+ left: result.type === CardType.LEARNING
+ ? SM2_CONFIG.LEARNING_STEPS.length * 1000
+ : 0,
mod: Math.floor(Date.now() / 1000),
};
scheduled = {
@@ -284,7 +375,9 @@ export async function serviceAnswerCard(
factor: result.newFactor,
reps: card.reps + 1,
lapses: result.newLapses,
- left: result.type === CardType.RELEARNING ? SM2_CONFIG.LEARNING_STEPS.length * 1000 + SM2_CONFIG.LEARNING_STEPS.length : 0,
+ left: result.type === CardType.RELEARNING
+ ? SM2_CONFIG.RELEARNING_STEPS.length * 1000
+ : 0,
mod: Math.floor(Date.now() / 1000),
};
scheduled = {
@@ -382,3 +475,11 @@ export async function serviceDeleteCard(cardId: bigint): Promise {
log.info("Deleting card", { cardId: cardId.toString() });
await repoDeleteCard(cardId);
}
+
+export async function serviceCheckCardOwnership(
+ input: ServiceInputCheckCardOwnership,
+): Promise {
+ log.debug("Checking card ownership", { cardId: input.cardId.toString() });
+ const ownerId = await repoGetCardDeckOwnerId(input.cardId);
+ return ownerId === input.userId;
+}
diff --git a/src/modules/deck/deck-service.ts b/src/modules/deck/deck-service.ts
index a3a276a..258401d 100644
--- a/src/modules/deck/deck-service.ts
+++ b/src/modules/deck/deck-service.ts
@@ -1,5 +1,3 @@
-"use server";
-
import { createLogger } from "@/lib/logger";
import {
ServiceInputCreateDeck,
diff --git a/src/modules/dictionary/dictionary-repository-dto.ts b/src/modules/dictionary/dictionary-repository-dto.ts
index 9536a42..cbc9cae 100644
--- a/src/modules/dictionary/dictionary-repository-dto.ts
+++ b/src/modules/dictionary/dictionary-repository-dto.ts
@@ -1,5 +1,3 @@
-import { TSharedItem } from "@/shared/dictionary-type";
-
export type RepoInputCreateDictionaryLookUp = {
userId?: string;
text: string;
@@ -8,7 +6,29 @@ export type RepoInputCreateDictionaryLookUp = {
dictionaryItemId?: number;
};
-export type RepoOutputSelectLastLookUpResult = TSharedItem & {id: number} | null;
+export type RepoOutputSelectLastLookUpResultEntry = {
+ id: number;
+ itemId: number;
+ ipa: string | null;
+ definition: string;
+ partOfSpeech: string | null;
+ example: string;
+ createdAt: Date;
+ updatedAt: Date;
+};
+
+export type RepoOutputSelectLastLookUpResultItem = {
+ id: number;
+ frequency: number;
+ standardForm: string;
+ queryLang: string;
+ definitionLang: string;
+ createdAt: Date;
+ updatedAt: Date;
+ entries: RepoOutputSelectLastLookUpResultEntry[];
+};
+
+export type RepoOutputSelectLastLookUpResult = RepoOutputSelectLastLookUpResultItem | null;
export type RepoInputCreateDictionaryItem = {
standardForm: string;
diff --git a/src/modules/dictionary/dictionary-repository.ts b/src/modules/dictionary/dictionary-repository.ts
index 7d76735..b8c5489 100644
--- a/src/modules/dictionary/dictionary-repository.ts
+++ b/src/modules/dictionary/dictionary-repository.ts
@@ -1,6 +1,5 @@
import { stringNormalize } from "@/utils/string";
import {
- RepoInputCreateDictionaryEntry,
RepoInputCreateDictionaryEntryWithoutItemId,
RepoInputCreateDictionaryItem,
RepoInputCreateDictionaryLookUp,
@@ -30,22 +29,12 @@ export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpR
createdAt: 'desc'
}
});
- if (result && result.dictionaryItem) {
- const item = result.dictionaryItem;
- return {
- id: item.id,
- standardForm: item.standardForm,
- entries: item.entries.filter(v => !!v).map(v => {
- return {
- ipa: v.ipa || undefined,
- definition: v.definition,
- partOfSpeech: v.partOfSpeech || undefined,
- example: v.example
- };
- })
- };
+
+ if (!result?.dictionaryItem) {
+ return null;
}
- return null;
+
+ return result.dictionaryItem;
}
export async function repoCreateLookUp(content: RepoInputCreateDictionaryLookUp) {
diff --git a/src/modules/dictionary/dictionary-service.ts b/src/modules/dictionary/dictionary-service.ts
index 1cfe232..82ed9cf 100644
--- a/src/modules/dictionary/dictionary-service.ts
+++ b/src/modules/dictionary/dictionary-service.ts
@@ -2,9 +2,23 @@ import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary/orchestrator"
import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository";
import { ServiceInputLookUp } from "./dictionary-service-dto";
import { createLogger } from "@/lib/logger";
+import { RepoOutputSelectLastLookUpResultItem } from "./dictionary-repository-dto";
const log = createLogger("dictionary-service");
+function transformRawItemToSharedItem(rawItem: RepoOutputSelectLastLookUpResultItem) {
+ return {
+ id: rawItem.id,
+ standardForm: rawItem.standardForm,
+ entries: rawItem.entries.map(entry => ({
+ ipa: entry.ipa ?? undefined,
+ definition: entry.definition,
+ partOfSpeech: entry.partOfSpeech ?? undefined,
+ example: entry.example
+ }))
+ };
+}
+
export const serviceLookUp = async (dto: ServiceInputLookUp) => {
const {
text,
@@ -27,7 +41,6 @@ export const serviceLookUp = async (dto: ServiceInputLookUp) => {
definitionLang
);
- // 使用事务确保数据一致性
repoCreateLookUpWithItemAndEntries(
{
standardForm: response.standardForm,
@@ -47,18 +60,20 @@ export const serviceLookUp = async (dto: ServiceInputLookUp) => {
return response;
} else {
+ const transformedResult = transformRawItemToSharedItem(lastLookUpResult);
+
repoCreateLookUp({
userId: userId,
text: text,
queryLang: queryLang,
definitionLang: definitionLang,
- dictionaryItemId: lastLookUpResult.id
+ dictionaryItemId: transformedResult.id
}).catch(error => {
log.error("Failed to save dictionary data", { error: error instanceof Error ? error.message : String(error) });
});
return {
- standardForm: lastLookUpResult.standardForm,
- entries: lastLookUpResult.entries
+ standardForm: transformedResult.standardForm,
+ entries: transformedResult.entries
};
}
};
\ No newline at end of file
diff --git a/src/modules/ocr/ocr-action.ts b/src/modules/ocr/ocr-action.ts
index cdcb77f..0200580 100644
--- a/src/modules/ocr/ocr-action.ts
+++ b/src/modules/ocr/ocr-action.ts
@@ -1,5 +1,7 @@
"use server";
+import { headers } from "next/headers";
+import { auth } from "@/auth";
import { validate } from "@/utils/validate";
import { ValidateError } from "@/lib/errors";
import { createLogger } from "@/lib/logger";
@@ -13,8 +15,17 @@ export async function actionProcessOCR(
input: unknown
): Promise {
try {
+ const session = await auth.api.getSession({ headers: await headers() });
+ if (!session?.user?.id) {
+ log.warn("Unauthorized OCR attempt");
+ return { success: false, message: "Unauthorized" };
+ }
+
const validatedInput = validate(input, schemaActionInputProcessOCR);
- return serviceProcessOCR(validatedInput);
+ return serviceProcessOCR({
+ ...validatedInput,
+ userId: session.user.id,
+ });
} catch (e) {
if (e instanceof ValidateError) {
return { success: false, message: e.message };
diff --git a/src/modules/ocr/ocr-service-dto.ts b/src/modules/ocr/ocr-service-dto.ts
index fd5e41e..9e6ba4e 100644
--- a/src/modules/ocr/ocr-service-dto.ts
+++ b/src/modules/ocr/ocr-service-dto.ts
@@ -5,6 +5,7 @@ export const schemaServiceInputProcessOCR = z.object({
deckId: z.number().int().positive("Deck ID must be positive"),
sourceLanguage: z.string().optional(),
targetLanguage: z.string().optional(),
+ userId: z.string().min(1, "User ID is required"),
});
export type ServiceInputProcessOCR = z.infer;
diff --git a/src/modules/ocr/ocr-service.ts b/src/modules/ocr/ocr-service.ts
index fb1adfc..6815dee 100644
--- a/src/modules/ocr/ocr-service.ts
+++ b/src/modules/ocr/ocr-service.ts
@@ -1,12 +1,8 @@
-"use server";
-
import { executeOCR } from "@/lib/bigmodel/ocr/orchestrator";
-import { repoGetUserIdByDeckId } from "@/modules/deck/deck-repository";
-import { repoCreateNote, repoJoinFields } from "@/modules/note/note-repository";
-import { repoCreateCard } from "@/modules/card/card-repository";
-import { repoGetNoteTypesByUserId, repoCreateNoteType } from "@/modules/note-type/note-type-repository";
-import { auth } from "@/auth";
-import { headers } from "next/headers";
+import { serviceCheckOwnership } from "@/modules/deck/deck-service";
+import { serviceCreateNote } from "@/modules/note/note-service";
+import { serviceCreateCard } from "@/modules/card/card-service";
+import { serviceGetNoteTypesByUserId, serviceCreateNoteType } from "@/modules/note-type/note-type-service";
import { createLogger } from "@/lib/logger";
import type { ServiceInputProcessOCR, ServiceOutputProcessOCR } from "./ocr-service-dto";
import { NoteKind } from "../../../generated/prisma/enums";
@@ -16,7 +12,7 @@ const log = createLogger("ocr-service");
const VOCABULARY_NOTE_TYPE_NAME = "Vocabulary (OCR)";
async function getOrCreateVocabularyNoteType(userId: string): Promise {
- const existingTypes = await repoGetNoteTypesByUserId({ userId });
+ const existingTypes = await serviceGetNoteTypesByUserId({ userId });
const existing = existingTypes.find((nt) => nt.name === VOCABULARY_NOTE_TYPE_NAME);
if (existing) {
@@ -47,7 +43,7 @@ async function getOrCreateVocabularyNoteType(userId: string): Promise {
const css = ".card { font-family: Arial; font-size: 20px; text-align: center; color: black; background-color: white; }";
- const noteTypeId = await repoCreateNoteType({
+ const noteTypeId = await serviceCreateNoteType({
name: VOCABULARY_NOTE_TYPE_NAME,
kind: NoteKind.STANDARD,
css,
@@ -63,19 +59,17 @@ async function getOrCreateVocabularyNoteType(userId: string): Promise {
export async function serviceProcessOCR(
input: ServiceInputProcessOCR
): Promise {
- log.info("Processing OCR request", { deckId: input.deckId });
+ log.info("Processing OCR request", { deckId: input.deckId, userId: input.userId });
- const session = await auth.api.getSession({ headers: await headers() });
- if (!session?.user?.id) {
- log.warn("Unauthorized OCR attempt");
- return { success: false, message: "Unauthorized" };
- }
-
- const deckOwner = await repoGetUserIdByDeckId(input.deckId);
- if (deckOwner !== session.user.id) {
+ const isOwner = await serviceCheckOwnership({
+ deckId: input.deckId,
+ userId: input.userId
+ });
+
+ if (!isOwner) {
log.warn("Deck ownership mismatch", {
deckId: input.deckId,
- userId: session.user.id
+ userId: input.userId
});
return {
success: false,
@@ -110,33 +104,28 @@ export async function serviceProcessOCR(
const sourceLanguage = ocrResult.detectedSourceLanguage || input.sourceLanguage || "Unknown";
const targetLanguage = ocrResult.detectedTargetLanguage || input.targetLanguage || "Unknown";
- const noteTypeId = await getOrCreateVocabularyNoteType(session.user.id);
+ const noteTypeId = await getOrCreateVocabularyNoteType(input.userId);
let pairsCreated = 0;
for (const pair of ocrResult.pairs) {
try {
- const now = Date.now();
- const noteId = await repoCreateNote({
+ const { id: noteId } = await serviceCreateNote({
noteTypeId,
- userId: session.user.id,
+ userId: input.userId,
fields: [pair.word, pair.definition, sourceLanguage, targetLanguage],
tags: ["ocr"],
});
- await repoCreateCard({
- id: BigInt(now + pairsCreated),
+ await serviceCreateCard({
noteId,
deckId: input.deckId,
ord: 0,
- due: pairsCreated + 1,
});
- await repoCreateCard({
- id: BigInt(now + pairsCreated + 10000),
+ await serviceCreateCard({
noteId,
deckId: input.deckId,
ord: 1,
- due: pairsCreated + 1,
});
pairsCreated++;
diff --git a/src/modules/translator/translator-action.ts b/src/modules/translator/translator-action.ts
index f4799a7..a9cd218 100644
--- a/src/modules/translator/translator-action.ts
+++ b/src/modules/translator/translator-action.ts
@@ -7,8 +7,7 @@ import {
} from "./translator-action-dto";
import { ValidateError } from "@/lib/errors";
import { createLogger } from "@/lib/logger";
-import { serviceTranslateText } from "./translator-service";
-import { getAnswer } from "@/lib/bigmodel/llm";
+import { serviceTranslateText, serviceGenIPA, serviceGenLanguage } from "./translator-service";
const log = createLogger("translator-action");
@@ -40,67 +39,12 @@ export const actionTranslateText = async (
* @deprecated 保留此函数以支持旧代码(text-speaker 功能)
*/
export const genIPA = async (text: string) => {
- return (
- "[" +
- (
- await getAnswer(
- `
-${text}
-
-请生成以上文本的严式国际音标
-然后直接发给我
-不要附带任何说明
-不要擅自增减符号
-不许用"/"或者"[]"包裹
-`.trim(),
- )
- )
- .replaceAll("[", "")
- .replaceAll("]", "") +
- "]"
- );
+ return serviceGenIPA({ text });
};
/**
* @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}`
- }
- ]);
- return language.trim();
+ return serviceGenLanguage({ text });
};
diff --git a/src/modules/translator/translator-service-dto.ts b/src/modules/translator/translator-service-dto.ts
index e85a6bc..ffd4076 100644
--- a/src/modules/translator/translator-service-dto.ts
+++ b/src/modules/translator/translator-service-dto.ts
@@ -10,3 +10,17 @@ export type ServiceInputTranslateText = {
};
export type ServiceOutputTranslateText = TSharedTranslationResult;
+
+// DTO types for deprecated genIPA function
+export type ServiceInputGenIPA = {
+ text: string;
+};
+
+export type ServiceOutputGenIPA = string;
+
+// DTO types for deprecated genLanguage function
+export type ServiceInputGenLanguage = {
+ text: string;
+};
+
+export type ServiceOutputGenLanguage = string;
diff --git a/src/modules/translator/translator-service.ts b/src/modules/translator/translator-service.ts
index 7c964f9..a6a01f2 100644
--- a/src/modules/translator/translator-service.ts
+++ b/src/modules/translator/translator-service.ts
@@ -1,6 +1,14 @@
import { executeTranslation } from "@/lib/bigmodel/translator/orchestrator";
+import { getAnswer } from "@/lib/bigmodel/llm";
import { repoCreateTranslationHistory, repoSelectLatestTranslation } from "./translator-repository";
-import { ServiceInputTranslateText, ServiceOutputTranslateText } from "./translator-service-dto";
+import {
+ ServiceInputTranslateText,
+ ServiceOutputTranslateText,
+ ServiceInputGenIPA,
+ ServiceOutputGenIPA,
+ ServiceInputGenLanguage,
+ ServiceOutputGenLanguage,
+} from "./translator-service-dto";
import { createLogger } from "@/lib/logger";
const log = createLogger("translator-service");
@@ -71,3 +79,72 @@ export const serviceTranslateText = async (
};
}
};
+
+export const serviceGenIPA = async (
+ dto: ServiceInputGenIPA
+): Promise => {
+ const { text } = dto;
+ return (
+ "[" +
+ (
+ await getAnswer(
+ `
+${text}
+
+请生成以上文本的严式国际音标
+然后直接发给我
+不要附带任何说明
+不要擅自增减符号
+不许用"/"或者"[]"包裹
+`.trim(),
+ )
+ )
+ .replaceAll("[", "")
+ .replaceAll("]", "") +
+ "]"
+ );
+};
+
+export const serviceGenLanguage = async (
+ dto: ServiceInputGenLanguage
+): Promise => {
+ const { text } = dto;
+ 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}`
+ }
+ ]);
+ return language.trim();
+};