重构
This commit is contained in:
@@ -1,130 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export interface SignUpFormData {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SignUpState {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
errors?: {
|
||||
username?: string[];
|
||||
email?: string[];
|
||||
password?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function signUpAction(prevState: SignUpState, formData: FormData) {
|
||||
const email = formData.get("email") as string;
|
||||
const name = formData.get("name") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const redirectTo = formData.get("redirectTo") as string;
|
||||
|
||||
// 服务器端验证
|
||||
const errors: SignUpState['errors'] = {};
|
||||
|
||||
if (!email) {
|
||||
errors.email = ["邮箱是必填项"];
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
errors.email = ["请输入有效的邮箱地址"];
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
errors.username = ["姓名是必填项"];
|
||||
} else if (name.length < 2) {
|
||||
errors.username = ["姓名至少需要2个字符"];
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
errors.password = ["密码是必填项"];
|
||||
} else if (password.length < 8) {
|
||||
errors.password = ["密码至少需要8个字符"];
|
||||
}
|
||||
|
||||
// 如果有验证错误,返回错误状态
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "请修正表单中的错误",
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.api.signUpEmail({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
name
|
||||
}
|
||||
});
|
||||
|
||||
redirect(redirectTo || "/");
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: "注册失败,请稍后再试"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function signInAction(prevState: SignUpState, formData: FormData) {
|
||||
const email = formData.get("email") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const redirectTo = formData.get("redirectTo") as string;
|
||||
|
||||
// 服务器端验证
|
||||
const errors: SignUpState['errors'] = {};
|
||||
|
||||
if (!email) {
|
||||
errors.email = ["邮箱是必填项"];
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
errors.email = ["请输入有效的邮箱地址"];
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
errors.password = ["密码是必填项"];
|
||||
}
|
||||
|
||||
// 如果有验证错误,返回错误状态
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "请修正表单中的错误",
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.api.signInEmail({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
}
|
||||
});
|
||||
|
||||
redirect(redirectTo || "/");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('NEXT_REDIRECT')) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: "登录失败,请检查您的邮箱和密码"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOutAction() {
|
||||
await auth.api.signOut({
|
||||
headers: await headers()
|
||||
});
|
||||
|
||||
redirect("/auth");
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
import { DictLookUpResponse } from "@/lib/shared";
|
||||
|
||||
import { LookUpServiceOutputDto } from "@/modules/dictionary/dictionary-service-dto";
|
||||
import { analyzeInput } from "./stage1-inputAnalysis";
|
||||
import { determineSemanticMapping } from "./stage2-semanticMapping";
|
||||
import { generateStandardForm } from "./stage3-standardForm";
|
||||
import { generateEntries } from "./stage4-entriesGeneration";
|
||||
import { LookUpError } from "@/lib/errors";
|
||||
|
||||
/**
|
||||
* 词典查询主编排器
|
||||
*
|
||||
* 将多个独立的 LLM 调用串联起来,每个阶段都有代码层面的数据验证
|
||||
* 只要有一环失败,直接返回错误
|
||||
*/
|
||||
export async function executeDictionaryLookup(
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string
|
||||
): Promise<DictLookUpResponse> {
|
||||
): Promise<LookUpServiceOutputDto> {
|
||||
try {
|
||||
// ========== 阶段 1:输入分析 ==========
|
||||
console.log("[阶段1] 开始输入分析...");
|
||||
@@ -80,7 +74,7 @@ export async function executeDictionaryLookup(
|
||||
console.log("[阶段4] 词条生成完成:", entriesResult);
|
||||
|
||||
// ========== 组装最终结果 ==========
|
||||
const finalResult: DictLookUpResponse = {
|
||||
const finalResult: LookUpServiceOutputDto = {
|
||||
standardForm: standardFormResult.standardForm,
|
||||
entries: entriesResult.entries,
|
||||
};
|
||||
@@ -91,8 +85,7 @@ export async function executeDictionaryLookup(
|
||||
} catch (error) {
|
||||
console.error("[错误] 词典查询失败:", error);
|
||||
|
||||
// 任何阶段失败都返回错误(包含 reason)
|
||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||
throw errorMessage;
|
||||
throw new LookUpError(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAnswer } from "../zhipu";
|
||||
import { parseAIGeneratedJSON } from "@/lib/utils";
|
||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||
import { InputAnalysisResult } from "./types";
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAnswer } from "../zhipu";
|
||||
import { parseAIGeneratedJSON } from "@/lib/utils";
|
||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||
import { SemanticMappingResult } from "./types";
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAnswer } from "../zhipu";
|
||||
import { parseAIGeneratedJSON } from "@/lib/utils";
|
||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||
import { StandardFormResult } from "./types";
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAnswer } from "../zhipu";
|
||||
import { parseAIGeneratedJSON } from "@/lib/utils";
|
||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||
import { EntriesGenerationResult } from "./types";
|
||||
|
||||
/**
|
||||
@@ -1,62 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
TranslationHistoryArraySchema,
|
||||
TranslationHistorySchema,
|
||||
} from "@/lib/interfaces";
|
||||
import z from "zod";
|
||||
import { shallowEqual } from "../utils";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
||||
key: string,
|
||||
schema: T,
|
||||
) => {
|
||||
return {
|
||||
get: (): z.infer<T> => {
|
||||
try {
|
||||
if (!globalThis.localStorage) return [] as z.infer<T>;
|
||||
const item = globalThis.localStorage.getItem(key);
|
||||
|
||||
if (!item) return [] as z.infer<T>;
|
||||
|
||||
const rawData = JSON.parse(item) as z.infer<T>;
|
||||
const result = schema.safeParse(rawData);
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
logger.error(
|
||||
"Invalid data structure in localStorage:",
|
||||
result.error,
|
||||
);
|
||||
return [] as z.infer<T>;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Failed to parse ${key} data:`, e);
|
||||
return [] as z.infer<T>;
|
||||
}
|
||||
},
|
||||
set: (data: z.infer<T>) => {
|
||||
if (!globalThis.localStorage) return;
|
||||
globalThis.localStorage.setItem(key, JSON.stringify(data));
|
||||
return data;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const MAX_HISTORY_LENGTH = 50;
|
||||
export const tlso = getLocalStorageOperator<
|
||||
typeof TranslationHistoryArraySchema
|
||||
>("translator", TranslationHistoryArraySchema);
|
||||
|
||||
export const tlsoPush = (item: z.infer<typeof TranslationHistorySchema>) => {
|
||||
const oldHistory = tlso.get();
|
||||
if (oldHistory.some((v) => shallowEqual(v, item))) return oldHistory;
|
||||
|
||||
const newHistory = [...oldHistory, item].slice(-MAX_HISTORY_LENGTH);
|
||||
tlso.set(newHistory);
|
||||
|
||||
return newHistory;
|
||||
};
|
||||
2
src/lib/errors.ts
Normal file
2
src/lib/errors.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export class ValidateError extends Error { };
|
||||
export class LookUpError extends Error { };
|
||||
@@ -1,56 +0,0 @@
|
||||
import z from "zod";
|
||||
|
||||
export interface Word {
|
||||
word: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export interface Letter {
|
||||
letter: string;
|
||||
letter_name_ipa: string;
|
||||
letter_sound_ipa: string;
|
||||
roman_letter?: string;
|
||||
}
|
||||
export type SupportedAlphabets =
|
||||
| "japanese"
|
||||
| "english"
|
||||
| "esperanto"
|
||||
| "uyghur";
|
||||
export const TextSpeakerItemSchema = z.object({
|
||||
text: z.string(),
|
||||
ipa: z.string().optional(),
|
||||
language: z.string(),
|
||||
});
|
||||
export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
|
||||
|
||||
export const WordDataSchema = z.object({
|
||||
languages: z
|
||||
.tuple([z.string(), z.string()])
|
||||
.refine(([first, second]) => first !== second, {
|
||||
message: "Languages must be different",
|
||||
}),
|
||||
wordPairs: z
|
||||
.array(z.tuple([z.string(), z.string()]))
|
||||
.min(1, "At least one word pair is required")
|
||||
.refine(
|
||||
(pairs) => {
|
||||
return pairs.every(
|
||||
([first, second]) => first.trim() !== "" && second.trim() !== "",
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Word pairs cannot contain empty strings",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const TranslationHistorySchema = z.object({
|
||||
text1: z.string(),
|
||||
text2: z.string(),
|
||||
language1: z.string(),
|
||||
language2: z.string(),
|
||||
});
|
||||
|
||||
export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema);
|
||||
|
||||
export type WordData = z.infer<typeof WordDataSchema>;
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 统一的日志工具
|
||||
* 在生产环境中可以通过环境变量控制日志级别
|
||||
*/
|
||||
|
||||
type LogLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const logger = {
|
||||
error: (message: string, error?: unknown) => {
|
||||
if (isDevelopment) {
|
||||
console.error(message, error);
|
||||
}
|
||||
// 在生产环境中,这里可以发送到错误追踪服务(如 Sentry)
|
||||
},
|
||||
|
||||
warn: (message: string, data?: unknown) => {
|
||||
if (isDevelopment) {
|
||||
console.warn(message, data);
|
||||
}
|
||||
},
|
||||
|
||||
info: (message: string, data?: unknown) => {
|
||||
if (isDevelopment) {
|
||||
console.info(message, data);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { executeDictionaryLookup } from "./dictionary";
|
||||
import { createLookUp, createPhrase, createWord, createPhraseEntry, createWordEntry, selectLastLookUp } from "../services/dictionaryService";
|
||||
import { DictLookUpRequest, DictWordResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared";
|
||||
import { lookUpValidation } from "@/lib/shared/validations/dictionaryValidations";
|
||||
|
||||
const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
|
||||
if (isDictPhraseResponse(res)) {
|
||||
// 先创建 Phrase
|
||||
const phrase = await createPhrase({
|
||||
standardForm: res.standardForm,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang,
|
||||
});
|
||||
|
||||
// 创建 Lookup
|
||||
await createLookUp({
|
||||
userId: req.userId,
|
||||
text: req.text,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang,
|
||||
dictionaryPhraseId: phrase.id,
|
||||
});
|
||||
|
||||
// 创建 Entries
|
||||
for (const entry of res.entries) {
|
||||
await createPhraseEntry({
|
||||
phraseId: phrase.id,
|
||||
definition: entry.definition,
|
||||
example: entry.example,
|
||||
});
|
||||
}
|
||||
} else if (isDictWordResponse(res)) {
|
||||
// 先创建 Word
|
||||
const word = await createWord({
|
||||
standardForm: (res as DictWordResponse).standardForm,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang,
|
||||
});
|
||||
|
||||
// 创建 Lookup
|
||||
await createLookUp({
|
||||
userId: req.userId,
|
||||
text: req.text,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang,
|
||||
dictionaryWordId: word.id,
|
||||
});
|
||||
|
||||
// 创建 Entries
|
||||
for (const entry of (res as DictWordResponse).entries) {
|
||||
await createWordEntry({
|
||||
wordId: word.id,
|
||||
ipa: entry.ipa,
|
||||
definition: entry.definition,
|
||||
partOfSpeech: entry.partOfSpeech,
|
||||
example: entry.example,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询单词或短语
|
||||
*
|
||||
* 使用模块化的词典查询系统,将提示词拆分为6个阶段:
|
||||
* - 阶段0:基础系统提示
|
||||
* - 阶段1:输入解析与语言识别
|
||||
* - 阶段2:跨语言语义映射决策
|
||||
* - 阶段3:standardForm 生成与规范化
|
||||
* - 阶段4:释义与词条生成
|
||||
* - 阶段5:错误处理
|
||||
* - 阶段6:最终输出封装
|
||||
*/
|
||||
export const lookUp = async (req: DictLookUpRequest): Promise<DictLookUpResponse> => {
|
||||
const {
|
||||
text,
|
||||
queryLang,
|
||||
forceRelook = false,
|
||||
definitionLang,
|
||||
userId
|
||||
} = req;
|
||||
|
||||
lookUpValidation(req);
|
||||
|
||||
const lastLookUp = await selectLastLookUp({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang
|
||||
});
|
||||
|
||||
if (forceRelook || !lastLookUp) {
|
||||
// 使用新的模块化查询系统
|
||||
const response = await executeDictionaryLookup(
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang
|
||||
);
|
||||
|
||||
saveResult({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
userId,
|
||||
forceRelook
|
||||
}, response);
|
||||
|
||||
return response;
|
||||
} else {
|
||||
// 从数据库返回缓存的结果
|
||||
if (lastLookUp.dictionaryWordId) {
|
||||
createLookUp({
|
||||
userId: userId,
|
||||
text: text,
|
||||
queryLang: queryLang,
|
||||
definitionLang: definitionLang,
|
||||
dictionaryWordId: lastLookUp.dictionaryWordId,
|
||||
});
|
||||
return {
|
||||
standardForm: lastLookUp.dictionaryWord!.standardForm,
|
||||
entries: lastLookUp.dictionaryWord!.entries
|
||||
};
|
||||
} else if (lastLookUp.dictionaryPhraseId) {
|
||||
createLookUp({
|
||||
userId: userId,
|
||||
text: text,
|
||||
queryLang: queryLang,
|
||||
definitionLang: definitionLang,
|
||||
dictionaryPhraseId: lastLookUp.dictionaryPhraseId
|
||||
});
|
||||
return {
|
||||
standardForm: lastLookUp.dictionaryPhrase!.standardForm,
|
||||
entries: lastLookUp.dictionaryPhrase!.entries
|
||||
};
|
||||
} else {
|
||||
throw "错误D101";
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,253 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAnswer } from "./zhipu";
|
||||
import { selectLatestTranslation, createTranslationHistory } from "../services/translatorService";
|
||||
import { TranslateTextInput, TranslateTextOutput, TranslationLLMResponse } from "../services/types";
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 translateText 函数代替
|
||||
* 保留此函数以支持旧代码(text-speaker 功能)
|
||||
*/
|
||||
export const genIPA = async (text: string) => {
|
||||
return (
|
||||
"[" +
|
||||
(
|
||||
await getAnswer(
|
||||
`
|
||||
<text>${text}</text>
|
||||
|
||||
请生成以上文本的严式国际音标
|
||||
然后直接发给我
|
||||
不要附带任何说明
|
||||
不要擅自增减符号
|
||||
不许用"/"或者"[]"包裹
|
||||
`.trim(),
|
||||
)
|
||||
)
|
||||
.replaceAll("[", "")
|
||||
.replaceAll("]", "") +
|
||||
"]"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 translateText 函数代替
|
||||
* 保留此函数以支持旧代码(text-speaker 功能)
|
||||
*/
|
||||
export const genLocale = async (text: string) => {
|
||||
return await getAnswer(
|
||||
`
|
||||
<text>${text}</text>
|
||||
|
||||
推断以上文本的地区(locale)
|
||||
然后直接发给我
|
||||
形如如zh-CN
|
||||
不要附带任何说明
|
||||
不要擅自增减符号
|
||||
`.trim(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 translateText 函数代替
|
||||
* 保留此函数以支持旧代码(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();
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 translateText 函数代替
|
||||
* 保留此函数以支持旧代码(text-speaker 功能)
|
||||
*/
|
||||
export const genTranslation = async (text: string, targetLanguage: string) => {
|
||||
|
||||
return await getAnswer(
|
||||
`
|
||||
<text>${text}</text>
|
||||
|
||||
请将以上文本翻译到 <target_language>${targetLanguage}</target_language>
|
||||
然后直接发给我
|
||||
不要附带任何说明
|
||||
不要擅自增减符号
|
||||
`.trim(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一的翻译函数
|
||||
* 一次调用生成所有信息,支持缓存查询
|
||||
*/
|
||||
export async function translateText(options: TranslateTextInput): Promise<TranslateTextOutput> {
|
||||
const {
|
||||
sourceText,
|
||||
targetLanguage,
|
||||
forceRetranslate = false,
|
||||
needIpa = true,
|
||||
userId,
|
||||
} = options;
|
||||
|
||||
// 1. 检查缓存(如果未强制重新翻译)并获取翻译数据
|
||||
let translatedData: TranslationLLMResponse | null = null;
|
||||
let fromCache = false;
|
||||
|
||||
if (!forceRetranslate) {
|
||||
const cached = await selectLatestTranslation({
|
||||
sourceText,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
if (cached && cached.translatedText && cached.sourceLanguage) {
|
||||
// 如果不需要 IPA,或缓存已有 IPA,使用缓存
|
||||
if (!needIpa || (cached.sourceIpa && cached.targetIpa)) {
|
||||
console.log("✅ 翻译缓存命中");
|
||||
translatedData = {
|
||||
translatedText: cached.translatedText,
|
||||
sourceLanguage: cached.sourceLanguage,
|
||||
targetLanguage: cached.targetLanguage,
|
||||
sourceIpa: cached.sourceIpa || undefined,
|
||||
targetIpa: cached.targetIpa || undefined,
|
||||
};
|
||||
fromCache = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果缓存未命中,调用 LLM 生成翻译
|
||||
if (!fromCache) {
|
||||
translatedData = await callTranslationLLM({
|
||||
sourceText,
|
||||
targetLanguage,
|
||||
needIpa,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 保存到数据库(不管缓存是否命中都保存)
|
||||
if (translatedData) {
|
||||
try {
|
||||
await createTranslationHistory({
|
||||
userId,
|
||||
sourceText,
|
||||
sourceLanguage: translatedData.sourceLanguage,
|
||||
targetLanguage: translatedData.targetLanguage,
|
||||
translatedText: translatedData.translatedText,
|
||||
sourceIpa: needIpa ? translatedData.sourceIpa : undefined,
|
||||
targetIpa: needIpa ? translatedData.targetIpa : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("保存翻译历史失败:", error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
sourceText,
|
||||
translatedText: translatedData!.translatedText,
|
||||
sourceLanguage: translatedData!.sourceLanguage,
|
||||
targetLanguage: translatedData!.targetLanguage,
|
||||
sourceIpa: needIpa ? (translatedData!.sourceIpa || "") : "",
|
||||
targetIpa: needIpa ? (translatedData!.targetIpa || "") : "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 LLM 生成翻译和相关数据
|
||||
*/
|
||||
async function callTranslationLLM(params: {
|
||||
sourceText: string;
|
||||
targetLanguage: string;
|
||||
needIpa: boolean;
|
||||
}): Promise<TranslationLLMResponse> {
|
||||
const { sourceText, targetLanguage, needIpa } = params;
|
||||
|
||||
console.log("🤖 调用 LLM 翻译");
|
||||
|
||||
let systemPrompt = "你是一个专业的翻译助手。请根据用户的要求翻译文本,并返回 JSON 格式的结果。\n\n返回的 JSON 必须严格符合以下格式:\n{\n \"translatedText\": \"翻译后的文本\",\n \"sourceLanguage\": \"源语言的标准英文名称(如 Chinese, English, Japanese)\",\n \"targetLanguage\": \"目标语言的标准英文名称\"";
|
||||
|
||||
if (needIpa) {
|
||||
systemPrompt += ",\n \"sourceIpa\": \"源文本的严式国际音标(用方括号包裹,如 [tɕɪn˥˩]\",\n \"targetIpa\": \"译文的严式国际音标(用方括号包裹)\"";
|
||||
}
|
||||
|
||||
systemPrompt += "}\n\n规则:\n1. 只返回 JSON,不要包含任何其他文字说明\n2. 语言名称必须是标准英文名称,首字母大写\n";
|
||||
|
||||
if (needIpa) {
|
||||
systemPrompt += "3. 国际音标必须用方括号 [] 包裹,使用严式音标\n";
|
||||
} else {
|
||||
systemPrompt += "3. 本次请求不需要生成国际音标\n";
|
||||
}
|
||||
|
||||
systemPrompt += needIpa ? "4. 确保翻译准确、自然" : "4. 确保翻译准确、自然";
|
||||
|
||||
const userPrompt = `请将以下文本翻译成 ${targetLanguage}:\n\n<text>${sourceText}</text>\n\n返回 JSON 格式的翻译结果。`;
|
||||
|
||||
const response = await getAnswer([
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt,
|
||||
},
|
||||
]);
|
||||
|
||||
// 解析 LLM 返回的 JSON
|
||||
try {
|
||||
// 清理响应:移除 markdown 代码块标记和多余空白
|
||||
let cleanedResponse = response
|
||||
.replace(/```json\s*\n/g, "") // 移除 ```json 开头
|
||||
.replace(/```\s*\n/g, "") // 移除 ``` 结尾
|
||||
.replace(/```\s*$/g, "") // 移除末尾的 ```
|
||||
.replace(/```json\s*$/g, "") // 移除末尾的 ```json
|
||||
.trim();
|
||||
|
||||
const parsed = JSON.parse(cleanedResponse) as TranslationLLMResponse;
|
||||
|
||||
// 验证必需字段
|
||||
if (!parsed.translatedText || !parsed.sourceLanguage || !parsed.targetLanguage) {
|
||||
throw new Error("LLM 返回的数据缺少必需字段");
|
||||
}
|
||||
|
||||
console.log("LLM 翻译成功");
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error("LLM 翻译失败:", error);
|
||||
console.error("原始响应:", response);
|
||||
throw new Error("翻译失败:无法解析 LLM 响应");
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
CreateDictionaryLookUpInput,
|
||||
DictionaryLookUpQuery,
|
||||
CreateDictionaryPhraseInput,
|
||||
CreateDictionaryPhraseEntryInput,
|
||||
CreateDictionaryWordInput,
|
||||
CreateDictionaryWordEntryInput
|
||||
} from "./types";
|
||||
import prisma from "../../db";
|
||||
|
||||
export async function selectLastLookUp(content: DictionaryLookUpQuery) {
|
||||
return prisma.dictionaryLookUp.findFirst({
|
||||
where: content,
|
||||
include: {
|
||||
dictionaryPhrase: {
|
||||
include: {
|
||||
entries: true
|
||||
}
|
||||
},
|
||||
dictionaryWord: {
|
||||
include: {
|
||||
entries: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPhraseEntry(content: CreateDictionaryPhraseEntryInput) {
|
||||
return prisma.dictionaryPhraseEntry.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createWordEntry(content: CreateDictionaryWordEntryInput) {
|
||||
return prisma.dictionaryWordEntry.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPhrase(content: CreateDictionaryPhraseInput) {
|
||||
return prisma.dictionaryPhrase.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createWord(content: CreateDictionaryWordInput) {
|
||||
return prisma.dictionaryWord.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createLookUp(content: CreateDictionaryLookUpInput) {
|
||||
return prisma.dictionaryLookUp.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { CreateFolderInput, UpdateFolderInput } from "./types";
|
||||
import prisma from "../../db";
|
||||
|
||||
export async function getFoldersByUserId(userId: string) {
|
||||
return prisma.folder.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function renameFolderById(id: number, newName: string) {
|
||||
return prisma.folder.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
name: newName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFoldersWithTotalPairsByUserId(userId: string) {
|
||||
const folders = await prisma.folder.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { pairs: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
return folders.map(folder => ({
|
||||
...folder,
|
||||
total: folder._count?.pairs ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createFolder(folder: CreateFolderInput) {
|
||||
return prisma.folder.create({
|
||||
data: folder,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteFolderById(id: number) {
|
||||
return prisma.folder.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateFolderById(id: number, data: UpdateFolderInput) {
|
||||
return prisma.folder.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserIdByFolderId(id: number) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
return folder?.userId;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { CreatePairInput, UpdatePairInput } from "./types";
|
||||
import prisma from "../../db";
|
||||
|
||||
export async function createPair(data: CreatePairInput) {
|
||||
return prisma.pair.create({
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePairById(id: number) {
|
||||
return prisma.pair.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePairById(
|
||||
id: number,
|
||||
data: UpdatePairInput,
|
||||
) {
|
||||
return prisma.pair.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPairCountByFolderId(folderId: number) {
|
||||
return prisma.pair.count({
|
||||
where: {
|
||||
folderId: folderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPairsByFolderId(folderId: number) {
|
||||
return prisma.pair.findMany({
|
||||
where: {
|
||||
folderId: folderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./types";
|
||||
import prisma from "../../db";
|
||||
|
||||
/**
|
||||
* 创建翻译历史记录
|
||||
*/
|
||||
export async function createTranslationHistory(data: CreateTranslationHistoryInput) {
|
||||
return prisma.translationHistory.create({
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最新的翻译记录
|
||||
* @param sourceText 源文本
|
||||
* @param targetLanguage 目标语言
|
||||
* @returns 最新的翻译记录,如果不存在则返回 null
|
||||
*/
|
||||
export async function selectLatestTranslation(query: TranslationHistoryQuery) {
|
||||
return prisma.translationHistory.findFirst({
|
||||
where: {
|
||||
sourceText: query.sourceText,
|
||||
targetLanguage: query.targetLanguage,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Service 层的自定义业务类型
|
||||
*
|
||||
* 这些类型用于替换 Prisma 生成的类型,提高代码的可维护性和抽象层次
|
||||
*/
|
||||
|
||||
// Folder 相关
|
||||
export interface CreateFolderInput {
|
||||
name: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface UpdateFolderInput {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Pair 相关
|
||||
export interface CreatePairInput {
|
||||
text1: string;
|
||||
text2: string;
|
||||
language1: string;
|
||||
language2: string;
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
folderId: number;
|
||||
}
|
||||
|
||||
export interface UpdatePairInput {
|
||||
text1?: string;
|
||||
text2?: string;
|
||||
language1?: string;
|
||||
language2?: string;
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
}
|
||||
|
||||
// Translation 相关
|
||||
export interface CreateTranslationHistoryInput {
|
||||
userId?: string;
|
||||
sourceText: string;
|
||||
sourceLanguage: string;
|
||||
targetLanguage: string;
|
||||
translatedText: string;
|
||||
sourceIpa?: string;
|
||||
targetIpa?: string;
|
||||
}
|
||||
|
||||
export interface TranslationHistoryQuery {
|
||||
sourceText: string;
|
||||
targetLanguage: string;
|
||||
}
|
||||
|
||||
// Dictionary 相关
|
||||
export interface CreateDictionaryLookUpInput {
|
||||
userId?: string;
|
||||
text: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
dictionaryWordId?: number;
|
||||
dictionaryPhraseId?: number;
|
||||
}
|
||||
|
||||
export interface DictionaryLookUpQuery {
|
||||
userId?: string;
|
||||
text?: string;
|
||||
queryLang?: string;
|
||||
definitionLang?: string;
|
||||
dictionaryWordId?: number;
|
||||
dictionaryPhraseId?: number;
|
||||
}
|
||||
|
||||
export interface CreateDictionaryWordInput {
|
||||
standardForm: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
}
|
||||
|
||||
export interface CreateDictionaryPhraseInput {
|
||||
standardForm: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
}
|
||||
|
||||
export interface CreateDictionaryWordEntryInput {
|
||||
wordId: number;
|
||||
ipa: string;
|
||||
definition: string;
|
||||
partOfSpeech: string;
|
||||
example: string;
|
||||
}
|
||||
|
||||
export interface CreateDictionaryPhraseEntryInput {
|
||||
phraseId: number;
|
||||
definition: string;
|
||||
example: string;
|
||||
}
|
||||
|
||||
// 翻译相关 - 统一翻译函数
|
||||
export interface TranslateTextInput {
|
||||
sourceText: string;
|
||||
targetLanguage: string;
|
||||
forceRetranslate?: boolean; // 默认 false
|
||||
needIpa?: boolean; // 默认 true
|
||||
userId?: string; // 可选用户 ID
|
||||
}
|
||||
|
||||
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 决定
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import prisma from "@/lib/db";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export async function createUserIfNotExists(email: string, name?: string | null) {
|
||||
const user = await prisma.user.upsert({
|
||||
where: {
|
||||
email: email,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: randomUUID(),
|
||||
email: email,
|
||||
name: name || "New User",
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getUserIdByEmail(email: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
return user ? user.id : null;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
export type DictLookUpRequest = {
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string,
|
||||
userId?: string,
|
||||
forceRelook?: boolean;
|
||||
};
|
||||
|
||||
export type DictWordEntry = {
|
||||
ipa: string;
|
||||
definition: string;
|
||||
partOfSpeech: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
export type DictPhraseEntry = {
|
||||
definition: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
export type DictWordResponse = {
|
||||
standardForm: string;
|
||||
entries: DictWordEntry[];
|
||||
};
|
||||
|
||||
export type DictPhraseResponse = {
|
||||
standardForm: string;
|
||||
entries: DictPhraseEntry[];
|
||||
};
|
||||
|
||||
export type DictLookUpResponse =
|
||||
| DictWordResponse
|
||||
| DictPhraseResponse;
|
||||
|
||||
// 类型守卫:判断是否为单词响应
|
||||
export function isDictWordResponse(
|
||||
response: DictLookUpResponse
|
||||
): response is DictWordResponse {
|
||||
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
||||
return entries.length > 0 && "ipa" in entries[0] && "partOfSpeech" in entries[0];
|
||||
}
|
||||
|
||||
// 类型守卫:判断是否为短语响应
|
||||
export function isDictPhraseResponse(
|
||||
response: DictLookUpResponse
|
||||
): response is DictPhraseResponse {
|
||||
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
||||
return entries.length > 0 && !("ipa" in entries[0] || "partOfSpeech" in entries[0]);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./dictionaryTypes";
|
||||
@@ -1,22 +0,0 @@
|
||||
import { DictLookUpRequest } from "@/lib/shared";
|
||||
|
||||
export const lookUpValidation = (req: DictLookUpRequest) => {
|
||||
const {
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
} = req;
|
||||
|
||||
if (text.length > 30)
|
||||
throw Error("The input should not exceed 30 characters.");
|
||||
if (queryLang.length > 20)
|
||||
throw Error("The query language should not exceed 20 characters.");
|
||||
if (definitionLang.length > 20)
|
||||
throw Error("The definition language should not exceed 20 characters.");
|
||||
if (queryLang.length > 20)
|
||||
throw Error("The query language should not exceed 20 characters.");
|
||||
if (queryLang.length > 20)
|
||||
throw Error("The query language should not exceed 20 characters.");
|
||||
if (queryLang.length > 20)
|
||||
throw Error("The query language should not exceed 20 characters.");
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 主题配色常量
|
||||
* 集中管理应用的品牌颜色
|
||||
*
|
||||
* 注意:Tailwind CSS 已有的标准颜色(gray、red 等)请直接使用 Tailwind 类名
|
||||
* 这里只定义项目独有的品牌色
|
||||
*/
|
||||
export const COLORS = {
|
||||
// ===== 主色调 =====
|
||||
/** 主绿色 - 应用主题色,用于页面背景、主要按钮 */
|
||||
primary: '#35786f',
|
||||
/** 悬停绿色 - 按钮悬停状态 */
|
||||
primaryHover: '#2d5f58'
|
||||
} as const;
|
||||
175
src/lib/utils.ts
175
src/lib/utils.ts
@@ -1,175 +0,0 @@
|
||||
export function isNonNegativeInteger(str: string): boolean {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
export function shallowEqual<T extends object>(obj1: T, obj2: T): boolean {
|
||||
const keys1 = Object.keys(obj1) as Array<keyof T>;
|
||||
const keys2 = Object.keys(obj2) as Array<keyof T>;
|
||||
|
||||
// 首先检查键的数量是否相同
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 然后逐个比较键值对
|
||||
for (const key of keys1) {
|
||||
if (obj1[key] !== obj2[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
export class SeededRandom {
|
||||
private seed: number;
|
||||
private readonly m: number = 0x80000000; // 2^31
|
||||
private readonly a: number = 1103515245;
|
||||
private readonly c: number = 12345;
|
||||
|
||||
constructor(seed?: number) {
|
||||
this.seed = seed || Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成0-1之间的随机数
|
||||
* @returns 0到1之间的随机浮点数
|
||||
*/
|
||||
next(): number {
|
||||
this.seed = (this.a * this.seed + this.c) % this.m;
|
||||
return this.seed / (this.m - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定范围的随机整数
|
||||
* @param min 最小值(包含)
|
||||
* @param max 最大值(包含)
|
||||
* @returns [min, max] 范围内的随机整数
|
||||
*/
|
||||
nextInt(min: number, max: number): number {
|
||||
if (min > max) {
|
||||
throw new Error('min must be less than or equal to max');
|
||||
}
|
||||
return Math.floor(this.next() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定范围的随机浮点数
|
||||
* @param min 最小值(包含)
|
||||
* @param max 最大值(不包含)
|
||||
* @returns [min, max) 范围内的随机浮点数
|
||||
*/
|
||||
nextFloat(min: number, max: number): number {
|
||||
if (min >= max) {
|
||||
throw new Error('min must be less than max');
|
||||
}
|
||||
return this.next() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成固定长度的随机数序列
|
||||
* @param length 序列长度
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @param type 生成类型:'integer' 或 'float'
|
||||
* @returns 随机数数组
|
||||
*/
|
||||
generateSequence(
|
||||
length: number,
|
||||
min: number = 0,
|
||||
max: number = 1,
|
||||
type: 'integer' | 'float' = 'integer'
|
||||
): number[] {
|
||||
const sequence: number[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (type === 'integer') {
|
||||
sequence.push(this.nextInt(min, max));
|
||||
} else {
|
||||
sequence.push(this.nextFloat(min, max));
|
||||
}
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置种子
|
||||
* @param newSeed 新的种子值
|
||||
*/
|
||||
reset(newSeed?: number): void {
|
||||
this.seed = newSeed || Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前种子值
|
||||
* @returns 当前种子
|
||||
*/
|
||||
getSeed(): number {
|
||||
return this.seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机布尔值
|
||||
* @param probability 为 true 的概率,默认 0.5
|
||||
* @returns 随机布尔值
|
||||
*/
|
||||
nextBoolean(probability: number = 0.5): boolean {
|
||||
if (probability < 0 || probability > 1) {
|
||||
throw new Error('probability must be between 0 and 1');
|
||||
}
|
||||
return this.next() < probability;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选择元素
|
||||
* @param array 源数组
|
||||
* @returns 随机选择的元素
|
||||
*/
|
||||
choice<T>(array: T[]): T {
|
||||
if (array.length === 0) {
|
||||
throw new Error('array cannot be empty');
|
||||
}
|
||||
const index = this.nextInt(0, array.length - 1);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 打乱数组(Fisher-Yates 洗牌算法)
|
||||
* @param array 要打乱的数组
|
||||
* @returns 打乱后的新数组
|
||||
*/
|
||||
shuffle<T>(array: T[]): T[] {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = this.nextInt(0, i);
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAIGeneratedJSON<T>(aiResponse: string): T {
|
||||
// 匹配 ```json ... ``` 包裹的内容
|
||||
const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[1].trim());
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
} else if (typeof error === 'string') {
|
||||
throw new Error(`Failed to parse JSON: ${error}`);
|
||||
} else {
|
||||
throw new Error('Failed to parse JSON: Unknown error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到json代码块,尝试直接解析整个字符串
|
||||
try {
|
||||
return JSON.parse(aiResponse.trim());
|
||||
} catch (error) {
|
||||
throw new Error('No valid JSON found in the response');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user