diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index e9a6a36b02..2c04d1b3d4 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -4,15 +4,16 @@ */ import { URLSearchParams } from 'node:url'; +import { TextDecoder } from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; -import { ApiError } from '../../error.js'; import { MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -94,32 +95,73 @@ export default class extends Endpoint { // eslint- let targetLang = ps.targetLang; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; - const params = new URLSearchParams(); - params.append('auth_key', this.serverSettings.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); + const systemPrompt = this.serverSettings.deeplAuthKey; - const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + const userPrompt = `pls translate to ${targetLang} ${note.text}`; + + const endpoint = this.serverSettings.deeplIsPro ? 'https://chatapi.neko.ci/api/qwen/chat' : 'https://chatapi.neko.ci/api/mthreads/chat'; + + const body = { + prompt: [systemPrompt, userPrompt], + temperature: 0.6, + top_p: 0.8, + max_tokens: 3000, + }; const res = await this.httpRequestService.send(endpoint, { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json, */*', + 'Content-Type': 'application/json', + Accept: '*/*', }, - body: params.toString(), + body: JSON.stringify(body), + timeout: 120_000, }); - const json = (await res.json()) as { - translations: { - detected_source_language: string; - text: string; - }[]; + let translatedText = ''; + + if (res.body) { + const decoder = new TextDecoder(); + let buffer = ''; + try { + outer: for await (const chunk of res.body) { + buffer += decoder.decode(chunk, { stream: true }); + const messages = buffer.split('\n\n'); + buffer = messages.pop() || ''; + + for (const msg of messages) { + if (msg.includes('event: complete')) { + const dataLine = msg.split('\n').find(line => line.startsWith('data:')); + if (dataLine) { + try { + const data = JSON.parse(dataLine.substring(6)); + if (Array.isArray(data) && data.length > 0) { + translatedText = data[0]; + res.body.destroy(); + break outer; + } + } catch { } + } + } + } + } + } catch { } + } + + const fullToHalfMap: { [key: string]: string } = { + ',': ',', '。': '.', '!': '!', '?': '?', ':': ':', ';': ';', + '(': '(', ')': ')', '“': '"', '”': '"', '‘': '\'', '’': '\'', + '【': '[', '】': ']', '、': ',', ' ': ' ', }; + let processedText = translatedText.replace(/[,。!?:;()“”‘’【】、 ]/g, m => fullToHalfMap[m]); + if (processedText.trim().length > 0) { + processedText = '\u200B\n\u200B' + processedText; + } + return { - sourceLang: json.translations[0].detected_source_language, - text: json.translations[0].text, + sourceLang: 'AI', + text: processedText, }; }); }