From 32c6d45cb0e373369e34e529fff084613c7a2c2f Mon Sep 17 00:00:00 2001 From: Gabe Date: Thu, 16 Oct 2025 23:51:49 +0800 Subject: [PATCH] feat: add custom api examples --- README.en.md | 6 +++ README.md | 6 +++ custom-api.md | 2 + custom-api_v2.md | 110 +++++++++++++++++++++++++++++++++++++++++++++ src/apis/trans.js | 5 ++- src/config/api.js | 8 ++-- src/config/i18n.js | 12 ++--- 7 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 custom-api_v2.md diff --git a/README.en.md b/README.en.md index 76d9327..fe2620e 100644 --- a/README.en.md +++ b/README.en.md @@ -147,6 +147,12 @@ If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translato Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent. +### How to set up a hook function for a custom API + +Custom APIs are very powerful and flexible, and can theoretically connect to any translation API. + +Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md) + ## Future Plans This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions: diff --git a/README.md b/README.md index 751c148..ad0840e 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,12 @@ 油猴脚本需要增加域名白名单,否则不能发出请求。 +### 如何设置自定义接口的hook函数 + +自定义接口功能非常强大、灵活,理论可以接入任何翻译接口。 + +示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md) + ## 未来规划 本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向: diff --git a/custom-api.md b/custom-api.md index 1a60ffd..47072c4 100644 --- a/custom-api.md +++ b/custom-api.md @@ -1,5 +1,7 @@ # 自定义接口示例(本文档已过期,新版不再适用) +V2版的示例请查看这里:[custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md) + 以下示例为网友提供,仅供学习参考。 ## 本地运行 Seed-X-PPO-7B 量化模型 diff --git a/custom-api_v2.md b/custom-api_v2.md new file mode 100644 index 0000000..5c08d78 --- /dev/null +++ b/custom-api_v2.md @@ -0,0 +1,110 @@ +# 自定义接口示例 + +## 谷歌翻译接口 + +URL + +``` +https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN +``` + +Request Hook + +```js +async (args) => { + const url = args.url.replace("{{text}}", args.texts[0]); + const method = "GET"; + return { url, method }; +}; +``` + +Response Hook + +```js +async ({ res }) => { + return { translations: [[res.sentences[0].trans]] }; +}; +``` + + +## Ollama + +* 注意 ollama 启动参数需要添加环境变量 `OLLAMA_ORIGINS=*` +* 检查环境变量生效命令:`systemctl show ollama | grep OLLAMA_ORIGINS` + +URL + +``` +http://localhost:11434/v1/chat/completions +``` + +Request Hook + +```js +async (args) => { + const url = args.url; + const method = "POST"; + const headers = { "Content-type": "application/json" }; + const body = { + model: "gemma3", + messages: [ + { + role: "system", + content: + 'Act as a translation API. Output a single raw JSON object only. No extra text or fences.\n\nInput:\n{"targetLanguage":"","title":"","description":"","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":""}\n\nOutput:\n{"translations":[{"id":1,"text":"...","sourceLanguage":""}]}\n\nRules:\n1. Use title/description for context only; do not output them.\n2. Keep id, order, and count of segments.\n3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., , ). Translate inner text only.\n4. Highest priority: Follow \'glossary\'. Use value for translation; if value is "", keep the key.\n5. Do not translate: content in ,
, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].\n6.  Apply the specified tone to the translation.\n7.  Detect sourceLanguage for each segment.\n8.  Return empty or unchanged inputs as is.\n\nExample:\nInput: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A React component."}],"glossary":{"component":"组件","React":""}}\nOutput: {"translations":[{"id":1,"text":"一个React组件","sourceLanguage":"en"}]}\n\nFail-safe: On any error, return {"translations":[]}.',
+      },
+      {
+        role: "user",
+        content: JSON.stringify({
+          targetLanguage: args.to,
+          segments: args.texts.map((text, id) => ({ id, text })),
+          glossary: {},
+        }),
+      },
+    ],
+    temperature: 0,
+    max_tokens: 20480,
+    think: false,
+    stream: false,
+  };
+
+  return { url, body, headers, method };
+};
+```
+
+Response Hook
+
+```js
+async ({ res }) => {
+  const extractJson = (raw) => {
+    const jsonRegex = /({.*}|\[.*\])/s;
+    const match = raw.match(jsonRegex);
+    return match ? match[0] : null;
+  };
+
+  const parseAIRes = (raw) => {
+    if (!raw) return [];
+
+    try {
+      const jsonString = extractJson(raw);
+      if (!jsonString) return [];
+
+      const data = JSON.parse(jsonString);
+      if (Array.isArray(data.translations)) {
+        return data.translations.map((item) => [
+          item?.text ?? "",
+          item?.sourceLanguage ?? "",
+        ]);
+      }
+    } catch (err) {
+      console.log("parseAIRes", err);
+    }
+
+    return [];
+  };
+
+  const translations = parseAIRes(res?.choices?.[0]?.message?.content);
+
+  return { translations };
+};
+```
diff --git a/src/apis/trans.js b/src/apis/trans.js
index 02614a6..5034123 100644
--- a/src/apis/trans.js
+++ b/src/apis/trans.js
@@ -98,8 +98,9 @@ const parseAIRes = (raw) => {
 
   try {
     const jsonString = extractJson(raw);
-    const data = JSON.parse(jsonString);
+    if (!jsonString) return [];
 
+    const data = JSON.parse(jsonString);
     if (Array.isArray(data.translations)) {
       // todo: 考虑序号id可能会打乱
       return data.translations.map((item) => [
@@ -925,7 +926,7 @@ export const handleTranslate = async (
     userMsg,
     ...apiSetting,
   });
-  if (!Array.isArray(result)) {
+  if (!result?.length) {
     throw new Error("tranlate got an unexpected result");
   }
 
diff --git a/src/config/api.js b/src/config/api.js
index 6dc4aec..d379975 100644
--- a/src/config/api.js
+++ b/src/config/api.js
@@ -409,16 +409,16 @@ Good morning.
 \`\`\``;
 
 const defaultRequestHook = `async (args, { url, body, headers, userMsg, method } = {}) => {
-  console.log("request hook args:", args);
+  console.log("request hook args:", { args, url, body, headers, userMsg, method });
   // return { url, body, headers, userMsg, method };
-}`;
+};`;
 
 const defaultResponseHook = `async ({ res, ...args }) => {
-  console.log("reaponse hook args:", res, args);
+  console.log("reaponse hook args:", { res, args });
   // const translations = [["你好", "zh"]];
   // const modelMsg = "";
   // return { translations, modelMsg };
-}`;
+};`;
 
 // 翻译接口默认参数
 const defaultApi = {
diff --git a/src/config/i18n.js b/src/config/i18n.js
index 9602053..6f4f8a9 100644
--- a/src/config/i18n.js
+++ b/src/config/i18n.js
@@ -137,46 +137,42 @@ ${customApiLangs}
 `;
 
 const requestHookHelperZH = `1、第一个参数包含如下字段:'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ...
-2、返回值必须是包含以下字段的对象: 'url', 'body', 'headers', 'userMsg', 'method'
+2、返回值必须是包含以下字段的对象: 'url', 'body', 'headers', 'method'
 3、如返回空值,则hook函数不会产生任何效果。
 
 // 示例
 async (args, { url, body, headers, userMsg, method } = {}) => {
-  console.log("request hook args:", args);
   return { url, body, headers, userMsg, method };
 }`;
 
 const requestHookHelperEN = `1. The first parameter contains the following fields: 'texts', 'from', 'to', 'url', 'key', 'model', 'systemPrompt', ...
-2. The return value must be an object containing the following fields: 'url', 'body', 'headers', 'userMsg', 'method'
+2. The return value must be an object containing the following fields: 'url', 'body', 'headers', 'method'
 3. If a null value is returned, the hook function will have no effect.
 
 // Example
 async (args, { url, body, headers, userMsg, method } = {}) => {
-  console.log("request hook args:", args);
   return { url, body, headers, userMsg, method };
 }`;
 
 const responsetHookHelperZH = `1、第一个参数包含如下字段:'res', ...
-2、返回值必须是包含以下字段的对象: 'translations', 'modelMsg' 
+2、返回值必须是包含以下字段的对象: 'translations'
   ('translations' 应为一个二维数组:[[译文, 源语言]])
 3、如返回空值,则hook函数不会产生任何效果。
 
 // 示例
 async ({ res, ...args }) => {
-  console.log("reaponse hook args:", res, args);
   const translations = [["你好", "zh"]];
   const modelMsg = "";
   return { translations, modelMsg };
 }`;
 
 const responsetHookHelperEN = `1. The first parameter contains the following fields: 'res', ...
-2. The return value must be an object containing the following fields: 'translations', 'modelMsg'
+2. The return value must be an object containing the following fields: 'translations'
   ('translations' should be a two-dimensional array: [[translation, source language]]).
 3. If a null value is returned, the hook function will have no effect.
 
 // Example
 async ({ res, ...args }) => {
-  console.log("reaponse hook args:", res, args);
   const translations = [["你好", "zh"]];
   const modelMsg = "";
   return { translations, modelMsg };