Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ The default service is Google Translate. Currently, we support:
| Gemini | Yes(free-) | [LLM-based](https://ai.google.dev/available_regions#available_languages) |
| Qwen-MT | Yes(free-) | [LLM-based](https://help.aliyun.com/zh/model-studio/user-guide/machine-translation) |
| Claude | Yes | [LLM-based](https://docs.anthropic.com/claude/docs/getting-started-with-the-claude-api) |
| DeepSeek | Yesy | [LLM-based](https://platform.deepseek.com/) |


> If the service you want is not yet supported, please post an issue.

Expand Down
9 changes: 9 additions & 0 deletions addon/locale/en-US/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ service-customgpt1=Custom GPT 1
service-customgpt2=Custom GPT 2
service-customgpt3=Custom GPT 3
service-azuregpt=AzureGPT
service-deepseek=DeepSeek
service-gemini=Gemini
service-qwenmt=Qwen-MT
service-claude=Claude
Expand Down Expand Up @@ -81,6 +82,14 @@ service-chatgpt-dialog-prompt=Prompt
service-chatgpt-dialog-stream=Stream
service-chatgpt-dialog-custom-request=Custom Request

service-deepseek-dialog-endPoint=API
service-deepseek-dialog-model=Model
service-deepseek-dialog-model-type-chat=Non-Thinking Mode(deepseek-chat)
service-deepseek-dialog-model-type-reasoner=Thinking Mode(deepseek-reasoner)
service-deepseek-dialog-system-prompt=System Prompt(leave blank to use the default system prompt)
service-deepseek-dialog-prompt=Prompt
service-deepseek-dialog-stream=Stream

service-azuregpt-dialog-endPoint=EndPoint
service-azuregpt-dialog-model=Name
service-azuregpt-dialog-temperature=Temp
Expand Down
9 changes: 9 additions & 0 deletions addon/locale/it-IT/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ service-customgpt1=Custom GPT 1
service-customgpt2=Custom GPT 2
service-customgpt3=Custom GPT 3
service-azuregpt=AzureGPT
service-deepseek=DeepSeek
service-gemini=Gemini
service-qwenmt=Qwen-MT
service-claude=Claude
Expand Down Expand Up @@ -81,6 +82,14 @@ service-chatgpt-dialog-prompt=Prompt
service-chatgpt-dialog-stream=Stream
service-chatgpt-dialog-custom-request=Custom Request

service-deepseek-dialog-endPoint=API
service-deepseek-dialog-model=Modello
service-deepseek-dialog-model-type-chat=Modalità non pensante(deepseek-chat)
service-deepseek-dialog-model-type-reasoner=Modalità pensante(deepseek-reasoner)
service-deepseek-dialog-system-prompt=Prompt di sistema (lasciare vuoto per usare il prompt di sistema predefinito)
service-deepseek-dialog-prompt=Prompt
service-deepseek-dialog-stream=Stream

service-azuregpt-dialog-endPoint=EndPoint
service-azuregpt-dialog-model=Nome
service-azuregpt-dialog-temperature=Temperatura
Expand Down
9 changes: 9 additions & 0 deletions addon/locale/zh-CN/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ service-customgpt1=自定义GPT1
service-customgpt2=自定义GPT2
service-customgpt3=自定义GPT3
service-azuregpt=AzureGPT
service-deepseek=DeepSeek
service-gemini=Gemini
service-qwenmt=Qwen-MT
service-claude=Claude
Expand Down Expand Up @@ -81,6 +82,14 @@ service-chatgpt-dialog-prompt=提示词
service-chatgpt-dialog-stream=流式输出
service-chatgpt-dialog-custom-request=自定义请求

service-deepseek-dialog-endPoint=接口
service-deepseek-dialog-model=模型
service-deepseek-dialog-model-type-chat=非思考模式(deepseek-chat)
service-deepseek-dialog-model-type-reasoner=思考模式(deepseek-reasoner)
service-deepseek-dialog-system-prompt=系统提示词(留空使用默认系统提示词)
service-deepseek-dialog-prompt=提示词
service-deepseek-dialog-stream=流式输出

service-azuregpt-dialog-endPoint=接口
service-azuregpt-dialog-model=部署名
service-azuregpt-dialog-temperature=温度
Expand Down
233 changes: 233 additions & 0 deletions src/modules/services/deepseek.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { getPref, getString, transformPromptWithContext, SecretValidateResult } from "../../utils";
import { TranslateService } from "./base";

const DEFAULT_API_URL = "https://api.deepseek.com/v1";
const DEFAULT_MODEL = "deepseek-chat";
interface ParsedResponse {
content: string;
finished: boolean;
}

function parseStreamResponse(obj: any): ParsedResponse {
// Handle OpenAI format (choices array with delta)
if (obj.choices && obj.choices[0]) {
const choice = obj.choices[0];
return {
content: choice.delta?.content || "",
finished:
choice.finish_reason !== undefined && choice.finish_reason !== null,
};
}
return { content: "", finished: false };
}

function parseNonStreamResponse(obj: any): string {
// Handle OpenAI format (choices array)
if (obj.choices && obj.choices[0]) {
return obj.choices[0].message.content || "";
}
return "";
}

const deepseekTranslate = async function (
data: Parameters<TranslateService["translate"]>[0],
stream?: boolean,
) {
function transformContent(
langFrom: string,
langTo: string,
sourceText: string,
) {
return transformPromptWithContext(
`deepSeek.prompt`,
langFrom,
langTo,
sourceText,
data,
);
}


function genSystemPrompt(): String {
const systemPrompt = (getPref("deepSeek.systemPrompt") as string) || "";
if (systemPrompt !== "") {
return systemPrompt
}

return `You are a highly skilled translation engine with expertise in academic paper translation. Your function is to translate academic texts into the ${data.langto}, ensuring the accurate translation of complex concepts and specialized terminology without altering the original academic tone or adding explanations.`
}

const apiURL = (getPref("deepSeek.endPoint") as string) || DEFAULT_API_URL;
const model = (getPref("deepSeek.model") as string) || DEFAULT_MODEL;
const streamMode = stream ?? (getPref("deepSeek.stream") as boolean);

const refreshHandler = addon.api.getTemporaryRefreshHandler({ task: data });

if (streamMode === false) {
data.result = getString("status-translating");
refreshHandler();
}


const streamCallback = (xmlhttp: XMLHttpRequest) => {
let preLength = 0;
let result = "";
let buffer = ""; // Buffer to store incomplete JSON fragments
xmlhttp.onprogress = (e: any) => {
// Only concatenate the new strings
const newResponse = e.target.response.slice(preLength);

// Handle OpenAI SSE format
let dataArray;
// Prepend buffer from previous incomplete chunk
const fullResponse = buffer + newResponse;
dataArray = fullResponse.split("data: ");
buffer = ""; // Reset buffer

for (let i = 0; i < dataArray.length; i++) {
const data = dataArray[i];
if (!data.trim()) continue;

try {
const obj = JSON.parse(data);
const { content, finished } = parseStreamResponse(obj);

result += content;
if (finished) {
break;
}
} catch {
// If parsing fails and this is the last item, it might be incomplete
// Save it to buffer for next iteration
if (i === dataArray.length - 1) {
buffer = "data: " + data;
}
continue;
}
}

// Clear timeouts caused by stream transfers
if (e.target.timeout) {
e.target.timeout = 0;
}

// Remove \n\n from the beginning of the data
data.result = result.replace(/^\n\n/, "");
preLength = e.target.response.length;

refreshHandler();
};
};

const nonStreamCallback = (xmlhttp: XMLHttpRequest) => {
xmlhttp.onload = () => {
try {
const responseObj = JSON.parse(xmlhttp.responseText);
const resultContent = parseNonStreamResponse(responseObj);
data.result = resultContent.replace(/^\n\n/, "");
} catch (error) {
return;
}
refreshHandler();
};
};

const xhr = await Zotero.HTTP.request("POST", apiURL, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${data.secret}`,
},
body: JSON.stringify({
model: model,
messages: [
{
role: "system",
content: genSystemPrompt(),
},
{
role: "user",
content: transformContent(data.langfrom, data.langto, data.raw),
},
],
stream: streamMode,
}),
responseType: "text",
requestObserver: (xmlhttp: XMLHttpRequest) => {
if (streamMode) {
streamCallback(xmlhttp);
} else {
nonStreamCallback(xmlhttp);
}
},
});

if (xhr?.status !== 200) {
throw `Request error: ${xhr?.status}`;
}
};

export const DeepSeek: TranslateService = {
id: "deepseek",
type: "sentence",

defaultSecret: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
secretValidator(secret: string): SecretValidateResult {
if (secret === DeepSeek.defaultSecret) { // Check if it is the default placeholder key
return {
secret,
status: false,
info: "Please input the API key.",
};
}

if (!secret.startsWith("sk-")) { // 检查密钥格式是否正确(必须以 'sk-' 开头)
return {
secret,
status: false,
info: `Invalid API key. It should start with 'sk-': ${secret}`
};

}

return {
secret,
status: true,
info: `API KEY: ${secret}`
};
},

async translate(data) {
return await deepseekTranslate(data);
},

config(settings) {
settings
.addTextSetting({
prefKey: "deepSeek.endPoint",
nameKey: "service-deepseek-dialog-endPoint",
})
.addSelectSetting({
prefKey: "deepSeek.model",
nameKey: "service-deepseek-dialog-model",
options: [
{ label: getString("service-deepseek-dialog-model-type-chat"), value: "deepseek-chat" },
{ label: getString("service-deepseek-dialog-model-type-reasoner"), value: "deepseek-reasoner" }
]
})
.addTextAreaSetting({
prefKey: "deepSeek.systemPrompt",
nameKey: "service-deepseek-dialog-system-prompt",
placeholder: getString("service-deepseek-dialog-system-prompt"),
})
.addTextAreaSetting({
prefKey: "deepSeek.prompt",
nameKey: "service-deepseek-dialog-prompt",
placeholder: getString("service-deepseek-dialog-prompt"),
})
.addCheckboxSetting({
prefKey: "deepSeek.stream",
nameKey: "service-deepseek-dialog-stream",
})

},
};
7 changes: 4 additions & 3 deletions src/modules/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { Aliyun } from "./aliyun";
import { Tencent } from "./tencent";
import { ChatGPT, customGPT1, customGPT2, customGPT3, azureGPT } from "./gpt";
import { DeepSeek } from "./deepseek";
import { Baidu } from "./baidu";
import { BaiduField } from "./baidufield";
import { Bing } from "./bing";
Expand Down Expand Up @@ -70,6 +71,7 @@ const register: TranslateService[] = [
customGPT2,
customGPT3,
azureGPT,
DeepSeek,
Haici,
HaiciDict,
Huoshan,
Expand Down Expand Up @@ -334,9 +336,8 @@ export class TranslationServices {
? item.annotationComment
: item.annotationText) || ""
).replace(regex, "");
let text = `${
currentText[currentText.length - 1] === "\n" ? "" : "\n"
}${splitChar}${task.result}${splitChar}\n`;
let text = `${currentText[currentText.length - 1] === "\n" ? "" : "\n"
}${splitChar}${task.result}${splitChar}\n`;
text = splitChar === "" ? text : `${currentText}${text}`;
item[
savePosition === "comment"
Expand Down
10 changes: 10 additions & 0 deletions typings/i10n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ export type FluentMessageId =
| 'service-chatgpt-dialog-prompt'
| 'service-chatgpt-dialog-stream'
| 'service-chatgpt-dialog-temperature'
| 'service-deepseek'
| 'service-deepseek-dialog-custom-request'
| 'service-deepseek-dialog-custom-request-description'
| 'service-deepseek-dialog-endPoint'
| 'service-deepseek-dialog-model'
| 'service-deepseek-dialog-model-type-chat'
| 'service-deepseek-dialog-model-type-reasoner'
| 'service-deepseek-dialog-system-prompt'
| 'service-deepseek-dialog-prompt'
| 'service-deepseek-dialog-stream'
| 'service-claude'
| 'service-claude-dialog-endPoint'
| 'service-claude-dialog-maxTokens'
Expand Down
5 changes: 5 additions & 0 deletions typings/prefs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ declare namespace _ZoteroTypes {
"claude.temperature": string;
"claude.prompt": string;
"claude.maxTokens": string;
"deepSeek.stream": boolean;
"deepSeek.endPoint": string;
"deepSeek.model": string;
"deepSeek.systemPrompt": string;
"deepSeek.prompt": string;
"xftrans.engine": string;
"tencent.region": string;
"tencent.projectId": string;
Expand Down
Loading