Skip to content

Commit

Permalink
feat: support stream output and change icon
Browse files Browse the repository at this point in the history
  • Loading branch information
jtsang4 committed Jun 3, 2023
1 parent 540a0c4 commit d62e055
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 72 deletions.
49 changes: 44 additions & 5 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,21 +126,41 @@ declare namespace Bob {
'yua' = '尤卡坦玛雅语',
'zu' = '祖鲁语',
}

type Languages = Array<keyof typeof LanguagesEnum>;
type supportLanguages = Languages;
type Language = keyof typeof LanguagesEnum;


interface DataPayload {
message: string;
}

interface Disposable {
dispose: () => void;
}

interface Signal {
send: (data?: DataPayload) => void;
subscribe: (callback: (data?: DataPayload) => void) => Disposable;
removeAllSubscriber: () => void;
}

// https://ripperhe.gitee.io/bob/#/plugin/quickstart/translate
type Translate = (query: TranslateQuery, completion: Completion) => void;
type completionResult = { result: Result };
type CompletionResult = { error: ServiceError };
type Completion = (args: completionResult | CompletionResult) => void;
type CompletionError = { error: ServiceError };
type Completion = (args: completionResult | CompletionError) => void;
type HandleStream = (args: completionResult) => void;
interface TranslateQuery {
text: string; // 需要翻译的文本
from: Language; // 用户选中的源语种标准码
to: Language; // 用户选中的目标语种标准码
detectFrom: Exclude<Language, 'auto'>; // 检测过后的源语种
detectTo: Exclude<Language, 'auto'>; // 检测过后的目标语种
cancelSignal: Signal,
onStream: HandleStream,
onCompletion: Completion; // 用于回调翻译结果的函数
}
interface OcrQuery {
from: Language; // 目前用户选中的源语言
Expand All @@ -164,7 +184,7 @@ declare namespace Bob {
author?: string; // 插件作者。
homepage?: string; // 插件主页网址。
appcast?: string; // 插件发布信息 URL。
minBobVersion?: string; // 最低支持本插件的 Bob 版本,建议填写您开发插件时候的调试插件的 Bob 版本,目前应该是 0.5.0。
minBobVersion?: string; // 最低支持本插件的 Bob 版本,建议填写您开发插件时候的调试插件的 Bob 版本,目前应该是 1.8.0。
options?: OptionObject[];
}
interface MenuObject {
Expand Down Expand Up @@ -208,6 +228,7 @@ declare namespace Bob {
request<T = any, R = HttpResponsePromise<T>>(config: HttpRequestConfig): Promise<R>;
get<T = any, R = HttpResponsePromise<T>>(config: HttpRequestConfig): Promise<R>;
post<T = any, R = HttpResponsePromise<T>>(config: HttpRequestConfig): Promise<R>;
streamRequest<T = any, R = HttpResponsePromise<T>>(config: HttpStreamRequestConfig): Promise<R>;
}
type HttpMethod =
| 'get'
Expand All @@ -233,6 +254,20 @@ declare namespace Bob {
handler?: (resp: HttpResponse) => void;
timeout?: number;
}

interface HttpStreamRequestConfig {
url: string;
method?: HttpMethod;
header?: any;
params?: any;
body?: any;
files?: HttpRequestFiles;
handler?: (resp: HttpResponse) => void;
cancelSignal?: Signal;
streamHandler?: (stream: { text: string, rawData: Data }) => void
timeout?: number;
}

interface HttpRequestFiles {
data: DataObject; // 二进制数据
name: string; // 上传表单中的名称
Expand Down Expand Up @@ -384,8 +419,12 @@ declare var $option: Bob.Option;
declare var $log: Bob.Log;
declare var $data: Bob.Data;
declare var $file: Bob.File;
declare var $signal: {
new: () => Bob.Signal;
};


declare function supportLanguages(): Bob.supportLanguages;
declare function translate(query: Bob.TranslateQuery, completion: Bob.Completion): void;
declare function translate(query: Bob.TranslateQuery): void;
declare function ocr(query: Bob.OcrQuery, completion: Bob.Completion): void;
declare function tts(query: Bob.TTSQuery, completion: Bob.Completion): void;
declare function tts(query: Bob.TTSQuery, completion: Bob.Completion): void;
Binary file modified src/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/info.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"identifier": "jtsang.claude.translator",
"version": "0.3.0",
"version": "0.4.0",
"category": "translate",
"name": "Claude Translator",
"summary": "Claude powered translator",
"icon": "",
"author": "jtsang <wtzeng1@gmail.com>",
"homepage": "https://github.com/jtsang4/bob-plugin-claude-translator",
"appcast": "https://raw.githubusercontent.com/jtsang4/bob-plugin-claude-translator/main/appcast.json",
"minBobVersion": "0.5.0",
"minBobVersion": "1.8.0",
"options": [
{
"identifier": "apiUrl",
Expand Down
164 changes: 99 additions & 65 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ function buildHeader(apiKey) {
* @returns {string}
*/
function generatePrompts(query) {
let userPrompt = `Translate from ${lang.langMap.get(query.detectFrom) || query.detectFrom} to ${lang.langMap.get(query.detectTo) || query.detectTo}`;
const translationPrefixPrompt = 'Translate below text in triple backticks'
let userPrompt = `${translationPrefixPrompt} from ${lang.langMap.get(query.detectFrom) || query.detectFrom} to ${lang.langMap.get(query.detectTo) || query.detectTo}`;

if (query.detectTo === "wyw" || query.detectTo === "yue") {
userPrompt = `翻译成${lang.langMap.get(query.detectTo) || query.detectTo}`;
userPrompt = `${translationPrefixPrompt} to ${lang.langMap.get(query.detectTo) || query.detectTo}`;
}

if (
Expand All @@ -37,25 +38,22 @@ function generatePrompts(query) {
query.detectFrom === "zh-Hant"
) {
if (query.detectTo === "zh-Hant") {
userPrompt = "翻译成繁体白话文";
userPrompt = `${translationPrefixPrompt} to traditional Chinese`;
} else if (query.detectTo === "zh-Hans") {
userPrompt = "翻译成简体白话文";
userPrompt = `${translationPrefixPrompt} to simplified Chinese`;
} else if (query.detectTo === "yue") {
userPrompt = "翻译成粤语白话文";
userPrompt = `${translationPrefixPrompt} to Cantonese`;
}
}
if (query.detectFrom === query.detectTo) {
if (query.detectTo === "zh-Hant" || query.detectTo === "zh-Hans") {
userPrompt = "润色此句";
} else {
userPrompt = "polish the sentence";
}
userPrompt = `Polish the sentence in triple backticks to ${query.detectTo}`;
}

userPrompt = `${userPrompt}:\n\n\
Here is the text in "<content>" tag:\n\n\
<content>${query.text}</content>.\n\n\
Reply in <response> tag. Do not include the <content> tag used for wrapping original text.`
userPrompt = `${userPrompt}:\n
\`\`\`
${query.text}
\`\`\`
Just give me the result without any extra words or symbol.`

return userPrompt;
}
Expand All @@ -69,30 +67,32 @@ Reply in <response> tag. Do not include the <content> tag used for wrapping orig
* max_tokens_to_sample: number;
* stop_sequences: string[]
* temperature: number;
* stream: boolean;
* }}
*/
function buildRequestBody(model, query) {
const prompt = generatePrompts(query);
return {
model,
prompt: `\n\nHuman: ${prompt}\n\nAssistant: OK, here is the result: <response>`,
prompt: `\n\nHuman: ${prompt}\n\nAssistant: OK, here is the result:`,
max_tokens_to_sample: 1000,
stop_sequences: [
"\n\nHuman:"
],
temperature: 0,
stream: true,
};
}

/**
* @param {Bob.Completion} completion
* @param {Bob.TranslateQuery} query
* @param {Bob.HttpResponse} result
* @returns {void}
*/
function handleError(completion, result) {
function handleError(query, result) {
const { statusCode } = result.response;
const reason = (statusCode >= 400 && statusCode < 500) ? "param" : "api";
completion({
query.onCompletion({
error: {
type: reason,
message: `接口响应错误 - ${result.data.detail}`,
Expand All @@ -102,53 +102,63 @@ function handleError(completion, result) {
}

/**
* @param {Bob.Completion} completion
* @param {Bob.TranslateQuery} query
* @param {Bob.HttpResponse} result
* @returns {void}
* @param {string} targetText
* @param {string} textFromResponse
* @returns {string}
*/
function handleResponse(completion, query, result) {
const { completion: resultText } = result.data;

if (!resultText) {
completion({
error: {
type: "api",
message: "接口未返回结果",
addtion: JSON.stringify(result),
},
});
return;
}

let targetText = resultText.trim();

if (targetText.startsWith('"') || targetText.startsWith("「")) {
targetText = targetText.slice(1);
}
if (targetText.endsWith('"') || targetText.endsWith("」")) {
targetText = targetText.slice(0, -1);
}
if (targetText.endsWith("</response>")) {
targetText = targetText.slice(0, -11);
function handleResponse(query, targetText, textFromResponse) {
let resultText = targetText;
if (textFromResponse !== '[DONE]') {
try {
const currentResponse = JSON.parse(textFromResponse);
const currentCompletion = currentResponse['completion'];
if (!currentCompletion) {
query.onCompletion({
error: {
type: "api",
message: "接口未返回结果",
addtion: textFromResponse,
},
});
return resultText;
}
resultText = currentCompletion;

if (resultText.startsWith('"') || resultText.startsWith("「")) {
resultText = resultText.slice(1);
}
if (resultText.endsWith('"') || resultText.endsWith("」")) {
resultText = resultText.slice(0, -1);
}
resultText = resultText.trim();
query.onStream({
result: {
from: query.detectFrom,
to: query.detectTo,
toParagraphs: [resultText],
},
});
return resultText;
} catch (err) {
query.onCompletion({
error: {
type: err._type || "param",
message: err.message || "JSON 解析错误",
addtion: err._addition,
},
});
}
}
targetText = targetText.trim();

completion({
result: {
from: query.detectFrom,
to: query.detectTo,
toParagraphs: targetText.split("\n"),
},
});
return resultText;
}

/**
* @type {Bob.Translate}
*/
function translate(query, completion) {
function translate(query) {
if (!lang.langMap.get(query.detectTo)) {
completion({
query.onCompletion({
error: {
type: "unsupportLanguage",
message: "不支持该语种",
Expand All @@ -157,9 +167,18 @@ function translate(query, completion) {
});
}

const { model, apiKeys, apiUrl } = $option;
const { model, apiKeys = '', apiUrl } = $option;

const apiKeySelection = apiKeys.split(",").map(key => key.trim());
if (!apiKeySelection.length) {
query.onCompletion({
error: {
type: "secretKey",
message: "配置错误 - 未填写 API Keys",
addtion: "请在插件配置中填写 API Keys",
},
})
}
const apiKey = apiKeySelection[Math.floor(Math.random() * apiKeySelection.length)];

const apiUrlPath = "/v1/complete";
Expand All @@ -168,20 +187,35 @@ function translate(query, completion) {
const body = buildRequestBody(model, query);

(async () => {
const result = await $http.request({
let targetText = '';
await $http.streamRequest({
method: "POST",
url: apiUrl + apiUrlPath,
header,
body,
cancelSignal: query.cancelSignal,
streamHandler: (streamData) => {
const line = streamData.text.split('\n')[0].trim();
const match = line.startsWith('data:') ? line.slice(5) : line;
const textFromResponse = match.trim();
targetText = handleResponse(query, targetText, textFromResponse);
},
handler: (result) => {
if (result.error || result.response.statusCode >= 400) {
handleError(query, result);
} else {
query.onCompletion({
result: {
from: query.detectFrom,
to: query.detectTo,
toParagraphs: [targetText],
},
});
}
},
});

if (result.error) {
handleError(completion, result);
} else {
handleResponse(completion, query, result);
}
})().catch((err) => {
completion({
query.onCompletion({
error: {
type: err._type || "unknown",
message: err._message || "未知错误",
Expand Down

0 comments on commit d62e055

Please sign in to comment.