diff --git a/src/info.json b/src/info.json index 4e661d4..ec8375c 100644 --- a/src/info.json +++ b/src/info.json @@ -1,6 +1,6 @@ { "identifier": "jtsang.claude.translator", - "version": "0.5.1", + "version": "0.5.2", "category": "translate", "name": "Claude Translator", "summary": "Claude powered translator", @@ -23,25 +23,37 @@ "title": "API KEY", "desc": "可以用英文逗号分割多个 API KEY 以实现额度加倍及负载均衡" }, + { + "identifier": "request_mode", + "type": "menu", + "title": "请求方式", + "defaultValue": "stream", + "menuValues": [ + { + "title": "流式请求", + "value": "stream" + } + ] + }, { "identifier": "model", "type": "menu", "title": "模型", - "defaultValue": "claude-v1.3", + "defaultValue": "claude-2.1", "menuValues": [ { - "title": "claude-v1.3 (recommended)", - "value": "claude-v1.3" + "title": "Claude 2.1", + "value": "claude-2.1" }, { - "title": "claude-2", - "value": "claude-2" + "title": "Claude 3 Opus", + "value": "claude-3-opus-20240229" }, { - "title": "claude-instant-1", - "value": "claude-instant-1" + "title": "Claude 3 Sonnet", + "value": "claude-3-sonnet-20240229" } ] } ] -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 5fcf918..1d778f5 100644 --- a/src/main.js +++ b/src/main.js @@ -1,110 +1,137 @@ //@ts-check -var lang = require("./lang.js"); +var lang = require('./lang.js'); function supportLanguages() { - return lang.supportLanguages.map(([standardLang]) => standardLang); + return lang.supportLanguages.map(([standardLang]) => standardLang); } /** * @param {string} apiKey - The authentication API key. * @returns {{ - * "Accept": string; - * "Content-Type": string; - * "x-api-key": string; - * "anthropic-version": string; + * "Accept": string; + * "Content-Type": string; + * "x-api-key": string; + * "anthropic-version": string; + * "anthropic-beta": string; * }} The header object. */ function buildHeader(apiKey) { - return { - "Accept": "application/json", - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": "2023-01-01", - }; + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'messages-2023-12-15', + }; } /** * @param {Bob.TranslateQuery} query * @returns {string} -*/ + */ function generatePrompts(query) { - const translationPrefixPrompt = 'Please translate below text' - let userPrompt = `${translationPrefixPrompt} from "${lang.langMap.get(query.detectFrom) || query.detectFrom}" to "${lang.langMap.get(query.detectTo) || query.detectTo}"`; + const translationPrefixPrompt = 'Please translate below text'; + 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 = `${translationPrefixPrompt} to "${lang.langMap.get(query.detectTo) || query.detectTo}"`; - } + if (query.detectTo === 'wyw' || query.detectTo === 'yue') { + userPrompt = `${translationPrefixPrompt} to "${ + lang.langMap.get(query.detectTo) || query.detectTo + }"`; + } - if ( - query.detectFrom === "wyw" || - query.detectFrom === "zh-Hans" || - query.detectFrom === "zh-Hant" - ) { - if (query.detectTo === "zh-Hant") { - userPrompt = `${translationPrefixPrompt} to traditional Chinese`; - } else if (query.detectTo === "zh-Hans") { - userPrompt = `${translationPrefixPrompt} to simplified Chinese`; - } else if (query.detectTo === "yue") { - userPrompt = `${translationPrefixPrompt} to Cantonese`; - } - } - if (query.detectFrom === query.detectTo) { - userPrompt = `Polish the sentence in triple backticks to "${query.detectTo}"`; + if ( + query.detectFrom === 'wyw' || + query.detectFrom === 'zh-Hans' || + query.detectFrom === 'zh-Hant' + ) { + if (query.detectTo === 'zh-Hant') { + userPrompt = `${translationPrefixPrompt} to traditional Chinese`; + } else if (query.detectTo === 'zh-Hans') { + userPrompt = `${translationPrefixPrompt} to simplified Chinese`; + } else if (query.detectTo === 'yue') { + userPrompt = `${translationPrefixPrompt} to Cantonese`; } + } + if (query.detectFrom === query.detectTo) { + userPrompt = `Polish the sentence in triple backticks to "${query.detectTo}"`; + } - userPrompt = `${userPrompt}:\n + userPrompt = `${userPrompt}:\n \`\`\` ${query.text} \`\`\` Do not add any content or symbols that does not exist in the original text. -` +`; - return userPrompt; + return userPrompt; } /** * @param {string} model * @param {Bob.TranslateQuery} query - * @returns {{ - * model: string; - * prompt: string; - * max_tokens_to_sample: number; - * stop_sequences: string[] - * temperature: number; - * stream: boolean; + * @returns {{ + * model: string; + * messages: {role: string; content: string}[]; + * max_tokens: number; + * stream: boolean; * }} -*/ + */ function buildRequestBody(model, query) { - const prompt = generatePrompts(query); - return { - model, - prompt: `\n\nHuman: ${prompt}\n\nAssistant: OK, this is the translation result: `, - max_tokens_to_sample: 100000, - stop_sequences: [ - "\n\nHuman:" - ], - temperature: 0.7, - stream: true, - }; + const prompt = generatePrompts(query); + $log.info(prompt); + return { + model, + messages: [ + { + role: 'user', + content: prompt, + }, + ], + max_tokens: 4096, + stream: true, + }; } /** * @param {Bob.TranslateQuery} query * @param {Bob.HttpResponse} result * @returns {void} -*/ + */ function handleError(query, result) { - const { statusCode } = result.response; - const reason = (statusCode >= 400 && statusCode < 500) ? "param" : "api"; - query.onCompletion({ - error: { - type: reason, - message: `接口响应错误 - ${result.data.detail}`, - addtion: JSON.stringify(result), - }, - }); + const { statusCode } = result.response; + const reason = statusCode >= 400 && statusCode < 500 ? 'param' : 'api'; + const errorMessage = + result.data && result.data.detail ? result.data.detail : '接口响应错误'; + + query.onCompletion({ + error: { + type: reason, + message: `${errorMessage}`, + addtion: JSON.stringify(result), + }, + }); +} +/** + * 解析流事件数据并根据事件类型进行处理 + * @param {string} line 从流中接收到的一行数据 + */ +function parseStreamData(line) { + // 解析事件类型 + const eventTypeMatch = line.match(/^event:\s*(.*)$/); + if (eventTypeMatch) { + return { eventType: eventTypeMatch[1] }; + } + // 解析数据内容 + const dataMatch = line.match(/^data:\s*(.*)$/); + if (dataMatch) { + const data = JSON.parse(dataMatch[1]); + return { data }; + } + return null; } /** @@ -112,132 +139,142 @@ function handleError(query, result) { * @param {string} targetText * @param {string} responseObj * @returns {string} -*/ + */ function handleResponse(query, targetText, responseObj) { - let resultText = targetText; - try { - const currentResponse = responseObj; - if (!currentResponse.hasOwnProperty('completion')) { - query.onCompletion({ - error: { - type: "api", - message: "接口未返回结果", - addtion: JSON.stringify(responseObj), - }, - }); - return resultText; + let resultText = targetText; + + try { + // @ts-ignore + const { type, delta, index } = responseObj; + + // 根据事件类型处理逻辑 + switch (type) { + case 'content_block_start': + // 如有必要,处理 content_block_start 事件 + break; + case 'content_block_delta': + // 处理文本变化 + if (delta && delta.type === 'text_delta') { + resultText += delta.text; } - resultText = currentResponse['completion']; query.onStream({ - result: { - from: query.detectFrom, - to: query.detectTo, - toParagraphs: [resultText], - }, + result: { + from: query.detectFrom, + to: query.detectTo, + toParagraphs: [resultText], + }, }); - return resultText; - } catch (err) { + break; + case 'content_block_stop': + // 如有必要,处理 content_block_stop 事件 + break; + case 'message_start': + // 如有必要,处理 message_start 事件 + break; + case 'message_delta': + // 可以在此处理停止原因等 message_delta 信息 + break; + case 'message_stop': + // 当消息流停止时,完成处理 query.onCompletion({ - error: { - type: err._type || "param", - message: err.message || "JSON 解析错误", - addtion: err._addition, - }, + result: { + from: query.detectFrom, + to: query.detectTo, + toParagraphs: [resultText], + }, }); + break; + default: + // 对无法识别的事件类型不做处理 + break; } return resultText; + } catch (err) { + // 错误处理 + query.onCompletion({ + error: { + type: err._type || 'param', + message: err.message || 'JSON 解析错误', + // @ts-ignore + addition: err._addition, + }, + }); + return resultText; + } } /** * @type {Bob.Translate} */ function translate(query) { - if (!lang.langMap.get(query.detectTo)) { - query.onCompletion({ - error: { - type: "unsupportLanguage", - message: "不支持该语种", - addtion: "不支持该语种", - }, - }); - } + if (!lang.langMap.get(query.detectTo)) { + query.onCompletion({ + error: { + type: 'unsupportLanguage', + message: '不支持该语种', + addtion: '不支持该语种', + }, + }); + } - const { model, apiKeys = '', apiUrl = 'https://api.anthropic.com' } = $option; + const { model, apiKeys = '', apiUrl = 'https://api.anthropic.com' } = $option; + const apiKeySelection = apiKeys.split(',').map((key) => key.trim()); - 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"; - - const header = buildHeader(apiKey); - const body = buildRequestBody(model, query); - - (async () => { - let targetText = ''; - let buffer = ''; - await $http.streamRequest({ - method: "POST", - url: apiUrl + apiUrlPath, - header, - body, - cancelSignal: query.cancelSignal, - streamHandler: (streamData) => { - const splitedText = streamData.text.split('\n'); - if (splitedText[0] && splitedText[0].trim() === 'event: completion') { - const line = splitedText[1].trim(); - const match = line.startsWith('data:') ? line.slice(5) : line; - const textFromResponse = match.trim(); - try { - if (textFromResponse !== '[DONE]' && !textFromResponse.includes('"completion":""')) { - const responseObj = JSON.parse(textFromResponse); - targetText = handleResponse(query, targetText, responseObj); - } - } catch (err) { - buffer = splitedText[1]; - } - } else if (splitedText[0] && !['event: completion', 'event: ping'].includes(splitedText[0].trim())) { - buffer += splitedText[0]; - const match = buffer.startsWith('data:') ? buffer.slice(5) : buffer; - const textFromResponse = match.trim(); - if (textFromResponse !== '[DONE]') { - const responseObj = JSON.parse(textFromResponse); - targetText = handleResponse(query, targetText, responseObj); - buffer = ''; - } - } - }, - handler: (result) => { - if (result.error || result.response.statusCode >= 400) { - handleError(query, result); - } else { - query.onCompletion({ - result: { - from: query.detectFrom, - to: query.detectTo, - toParagraphs: [targetText], - }, - }); - } - }, - }); - })().catch((err) => { - query.onCompletion({ - error: { - type: err._type || "unknown", - message: err._message || "未知错误", - addtion: err._addition, - }, - }); + 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/messages'; + const header = buildHeader(apiKey); + const body = buildRequestBody(model, query); + + (async () => { + let targetText = ''; + + await $http.streamRequest({ + method: 'POST', + url: apiUrl + apiUrlPath, + header, + body, + cancelSignal: query.cancelSignal, + streamHandler: (streamData) => { + const lines = streamData.text.split('\n'); + for (const line of lines) { + const parsedData = parseStreamData(line); + if (!parsedData) continue; // 如果解析不到数据则跳过 + + if (parsedData.eventType) { + // 根据事件类型做一些操作,例如记录日志等 + $log.info(`Received event: ${parsedData.eventType}`); + } else if (parsedData.data) { + // 这里调用 handleResponse 或其他函数处理具体数据 + targetText = handleResponse(query, targetText, parsedData.data); + } + } + }, + handler: (result) => { + if (result.error || result.response.statusCode >= 400) { + handleError(query, result); + } + }, + }); + })().catch((err) => { + query.onCompletion({ + error: { + type: err._type || 'unknown', + message: err._message || '未知错误', + addtion: err._addition, + }, }); + }); } exports.supportLanguages = supportLanguages;