diff --git a/LLM/baidu.go b/LLM/baidu.go deleted file mode 100644 index 56f0fc3..0000000 --- a/LLM/baidu.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "strings" - - baidu "github.com/anhao/go-ernie" - - "FluentRead/misc/log" - "FluentRead/models/constant" - "FluentRead/repo/db" -) - -func main() { - - ctx := context.Background() - count := 10 - tokenCount := 0 - - //trans, err := db.ListNotTranslated(ctx) - trans, err := db.ListTrans(ctx) - if err != nil { - log.Error("获取未翻译列表失败:", err) - return - } - transSize := len(trans) - - // 获取页面信息 - pageInfo, err := db.GetPageById(ctx, trans[0].PageId) - if err != nil { - log.Error("获取页面信息失败:", err) - return - } - - // 提示语 - systemMsg := strings.Replace(constant.CommonMsg, "{{site}}", pageInfo.Link, -1) - builder := strings.Builder{} - - // 准备模型 - client := baidu.NewDefaultClient(os.Getenv("BAIDU_KEY"), os.Getenv("BAIDU_SECRET")) - - for i := 0; i < transSize; i += count { - if i != 1000 { - continue - } - - fmt.Printf("\n新的翻译 %d >>>\n", (i/count)+1) - builder.Reset() - - end := i + count - if end > transSize { - end = transSize - } - - buildCount := 0 - for j := i; j < end; j++ { - replace := strings.Replace(trans[j].Source, "\n", " ", -1) - builder.WriteString(fmt.Sprintf("%s\n", replace)) - buildCount++ - } - - // 发起请求 - resp, err := client.CreateErnieBotChatCompletion(ctx, baidu.ErnieBotRequest{ - Messages: []baidu.ChatCompletionMessage{ - { - Role: baidu.MessageRoleUser, - Content: systemMsg + builder.String(), - }, - }, - }) - if err != nil { - log.Error("获取翻译失败:", err) - continue - } - tokenCount += resp.Usage.TotalTokens - fmt.Printf("本次token消耗数:%d, 总消耗数:%d\n%s\n", resp.Usage.TotalTokens, tokenCount, resp.Result) - } -} diff --git a/LLM/openai.go b/LLM/openai.go deleted file mode 100644 index 46a7853..0000000 --- a/LLM/openai.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "strings" - - "github.com/sashabaranov/go-openai" - - "FluentRead/misc/log" - "FluentRead/models/constant" - "FluentRead/repo/db" -) - -const count = 15 - -func main() { - ctx := context.Background() - client := openai.NewClient(os.Getenv("OPENAI_API_KEY")) - tokenCount := 0 - - trans, err := db.ListNotTranslated(ctx) - if err != nil { - log.Error("获取未翻译列表失败:", err) - return - } - transSize := len(trans) - - // 获取页面信息 - pageInfo, err := db.GetPageById(ctx, trans[0].PageId) - if err != nil { - log.Error("获取页面信息失败:", err) - return - } - - // 构建请求 - builder := strings.Builder{} - q := newQueue(openai.GPT3Dot5Turbo1106, 1) - q.systemRole = openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleSystem, - Content: strings.Replace(constant.SystemRoleMsg, "{{site}}", pageInfo.Link, -1), - } - - for i := 0; i < transSize; i += count { - - fmt.Printf("\n新的翻译 %d >>>\n", (i/count)+1) - builder.Reset() - - end := i + count - if end > transSize { - end = transSize - } - - buildCount := 0 - for j := i; j < end; j++ { - // 句子写入 builder - replace := strings.Replace(trans[j].Source, "\n", " ", -1) - builder.WriteString(fmt.Sprintf("%s\n", replace)) - buildCount++ - } - - q.push(openai.ChatCompletionMessage{Role: openai.ChatMessageRoleUser, Content: builder.String()}) - request := q.build() - resp, err := client.CreateChatCompletion(ctx, request) - if err != nil { - log.Error("LLM.CreateChatCompletion err:", err) - continue - } - - // 获取翻译结果 - split := strings.Split(resp.Choices[0].Message.Content, "\n") - if len(split) != buildCount { - log.Errorf("翻译结果数量不符,数量 len(split) %d != %d:%s", len(split), buildCount, split) - continue - } - - for k, translated := range split { - trans[i+k].Target = translated - trans[i+k].Translated = true - } - - if err = db.BatchUpdateTrans(ctx, trans[i:end]); err != nil { - log.Error("批量更新数据库失败:", err) - continue - } - - q.push(resp.Choices[0].Message) - tokenCount += resp.Usage.TotalTokens - fmt.Printf("本次token消耗数:%d, 总消耗数:%d\n%s\n", resp.Usage.TotalTokens, tokenCount, resp.Choices[0].Message.Content) - } -} diff --git a/LLM/openai_test.go b/LLM/openai_test.go deleted file mode 100644 index 06ab7d0..0000000 --- a/LLM/openai_test.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/LLM/queue.go b/LLM/queue.go deleted file mode 100644 index ae43594..0000000 --- a/LLM/queue.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "github.com/sashabaranov/go-openai" -) - -type queue struct { - request openai.ChatCompletionRequest - systemRole openai.ChatCompletionMessage - assistants []openai.ChatCompletionMessage - capacity int -} - -func newQueue(model string, capacity int) *queue { - return &queue{ - request: openai.ChatCompletionRequest{Model: model}, - assistants: make([]openai.ChatCompletionMessage, 0, capacity), - capacity: capacity, - } -} - -func (q *queue) push(assistant openai.ChatCompletionMessage) { - if len(q.assistants) >= q.capacity { - // 移除队首元素 - q.assistants = q.assistants[1:] - } - q.assistants = append(q.assistants, assistant) -} - -func (q *queue) pop() openai.ChatCompletionMessage { - if len(q.assistants) == 0 { - return openai.ChatCompletionMessage{} - } - assistant := q.assistants[0] - q.assistants = q.assistants[1:] - return assistant -} - -// 构建上下文对话的请求 -func (q *queue) build() openai.ChatCompletionRequest { - messages := make([]openai.ChatCompletionMessage, 0, q.capacity+1) - messages = append(messages, q.systemRole) - for _, v := range q.assistants { - messages = append(messages, v) - } - q.request.Messages = messages - return q.request -} diff --git a/README.md b/README.md index a90ec07..57d7a40 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# 流畅阅读 流畅阅读 +# 流畅阅读 流畅阅读 > Fluent Read,基于母语般的阅读体验 -拥有基于上下文语境的人工智能翻译引擎,为网站提供更加友好的翻译,让所有人都能够拥有基于母语般的阅读体验。 +流畅阅读是一款高效的浏览器翻译插件,可以将网页上的文字翻译成任何语言,方便、快捷、直观,支持人工智能引擎。 -sample-git-1.gif +流畅阅读采用 [Vue3](https://cn.vuejs.org/) + [Element-Plus](https://element-plus.org/) + [WXT](https://wxt.dev/) 框架编写,支持编译成可安装在绝大多数浏览器的插件。 -sample-5.gif +sample-git-1.gif # 安装指南 @@ -23,7 +23,7 @@ - 通用浏览器版本:[直接下载](https://bistutu.github.io/images/fr/FluentRead-0.0.1-chrome.zip) - Firefox(火狐)浏览器版本:[直接下载](https://bistutu.github.io/images/fr/FluentRead--0.0.2-firefox.zip) -sample-git-1.gifsample-git-2.gif +sample-git-1.gifsample-git-2.gif > 手机版如何安装? @@ -32,77 +32,55 @@ 1. 在浏览器中安装 [油猴插件](https://www.tampermonkey.net)(如已安装则跳过) 2. 在油猴中安装 [流畅阅读](https://greasyfork.org/zh-CN/scripts/482986-%E6%B5%81%E7%95%85%E9%98%85%E8%AF%BB) 插件 -# 如何使用? +# 特点介绍 -1. 打开油猴插件,**配置**你想要的翻译服务(如果选择 AI 翻译则需要填写 `token`)。 +1. 多种翻译方式: - sample-git-1.gif + 1. 快捷键翻译:将鼠标悬浮在文本上,并按下设定的快捷键即可翻译。 + 2. 滑动翻译:持续按住快捷键,同时用鼠标滑动选择需要翻译的文本区域。 -2. 翻译功能 +2. 支持缓存与回译: - 流畅阅读提供了 2 种翻译方式,分别为: + 为了避免重复翻译句子、减少请求次数,流畅阅读基于每个页面做了翻译缓存,你无需关心具体细节(蓝色表示正常翻译、绿色表示使用缓存),当你使用缓存时,插件不会发生网络请求。 - - 将鼠标悬浮在想要翻译的文本上面,按下快捷键进行翻译。 - - 持续按住快捷键,鼠标滑动至文本进行翻译。 + sample-git-1.gif -3. 缓存与回译功能 - - 为了避免 AI 模型重复翻译句子,流畅阅读基于每个页面做了缓存,你无需关心具体细节(蓝色表示正常翻译、绿色表示使用缓存)。 - - sample-git-1.gif - - 当你将鼠标悬浮在已翻译的文本上并按下快捷键时,会触发“回译”功能。 - - sample-git-1.gif - -4. 部分网站定制化翻译功能 - - 由于部分网站属于“较常”访问的站点,流畅阅读在编写时考虑到了这些场景,因此针对部分网站做了专门的翻译,这些站点包括但不限于: - - [OpenAI官网](https://openai.com/)、[Docker仓库](https://hub.docker.com)、[Maven仓库](https://mvnrepository.com/)、[Stackoverflow](https://stackoverflow.com/)、[GitHub Star 趋势生成网](https://star-history.com/)、[Coze人工智能助手](https://www.coze.com) - -# 为什么是流畅阅读? - -1. 程序开源、免费 - -2. 代码接受审查、不收集任何用户信息,保证数据安全 - -3. 支持常见国外或国产AI大模型,如: - - [chatGPT](https://platform.openai.com/)、[Gemini](https://gemini.google.com/)、[通义千问](https://dashscope.console.aliyun.com/overview)、[智谱清言](https://open.bigmodel.cn/)、[文心一言](https://cloud.baidu.com/wenxin.html)、[moonshot](https://www.moonshot.cn/) - -4. 支持 chatGPT 自定义 API 接口地址,支持使用国内代理访问 + 当你将鼠标悬浮在已翻译的文本上并按下快捷键时,会触发**回译功能**。 + + sample-git-1.gif # 常见问题解答
- 1、我该如何获取 token? -   如果你想获取各家大模型的 token,建议直接在百度或 Google 中以关键词 模型名称 + api 进行搜索。 + 1、我应该如何获取token? +   如果你想获取翻译服务的token,建议直接在百度或Google中以关键词 模型名称 + api 进行搜索。
- 2、为什么翻译的内容不准确? -  翻译可能不准确的原因有很多。首先,翻译引擎可能无法完全理解原文的上下文和细微的语言差异。其次,不同语言之间的语法结构和表达方式可能存在很大差异,这使得直接翻译时难以保留原文的全部含义。建议更换其他模型进行尝试 + 2、我的token会不会被别人窃取? +   不会。流畅阅读会将你输入的token全部保存在浏览器本地,没有任何第三方能够窃取你的token数据,包括我们自己。
3、我的数据会被中间人获取吗? -   流畅阅读代码完全开源,不会收集你的数据,你的所有翻译请求都会被直接转发至相应的官方接口。所以说,你的数据安全取决于你所使用的翻译服务提供商。 +   不会。流畅阅读不会收集你的数据,你的所有翻译请求都会被直接转发至翻译服务提供商,没有中间人可以获取你们之间的翻译记录。 +
+
+ 4、为什么翻译的内容不准确? +  翻译可能不准确的原因有很多。首先,翻译引擎可能无法完全理解原文的上下文和细微的语言差异。其次,不同语言之间的语法结构和表达方式可能存在很大差异,这使得直接翻译时难以保留原文的全部含义,建议更换其他模型进行尝试
- # 版本更新记录 +- 2024-04-01:使用 Vue3 + WXT 重构程序,重新发布 0.01 版本 - 2024-03-04:1.30版本更新 1. 增加 [DeepL](https://deepl.com/zh/) 翻译服务 - 2. 增加 [ollama](https://github.com/ollama/ollama) **本地**大模型支持 + 2. 增加 [ollama](https://github.com/ollama/ollama) 本地大模型支持 3. 兼容移动设备,实现“三指触摸”翻译 - 4. 优化缓存逻辑,默认翻译缓存 24h - 5. 新增快捷键 `F2` 删除翻译缓存 + 4. 优化缓存逻辑 - 2024-02-18:1.0版本发布。 1. 接入微软机器翻译 - 2. 接入 OpenAI、智谱清言、文心一言、通义千问、Gemini、moonshot 人工智能引擎 - 3. 增加鼠标快捷键操作 + 2. 接入 OpenAI、智谱清言、moonshot 等人工智能引擎 + 3. 增加快捷键翻译功能 4. 增加翻译缓存与回译功能 - 5. 去除讯飞翻译 -- 2024-01:0.6版本发布,增加讯飞翻译引擎 +- 2024-01:0.6版本发布 - ... - 2023-12:0.1版本发布 @@ -110,15 +88,26 @@ [GPL-3.0 license](https://github.com/Bistutu/FluentRead#) -# star 历史记录 +# 如何运行程序? + +```shell +git clone https://github.com/Bistutu/FluentRead.git +cd ./FluentRead +pnpm install && pnpm dev +``` -[![Star History Chart](https://api.star-history.com/svg?repos=Bistutu/FluentRead&type=Date)](https://star-history.com/#520250/FluentRead&Date) +```shell +# 打包为支持 Chromium 内核的插件 +pnpm zip +# 打包为支持 FireFox 内核的插件 +pnpm zip:firefox +``` # 赞赏码 -如果您希望向作者给予鼓励,请扫描下面二维码,您的名字将会出现在我们的赞助名单上。 +如果您希望向作者给予鼓励,请扫描下面二维码,您的名字将会出现在赞助名单上。 -wechat +wechat | 序号 | 🌼赞助者🌼 | 赞助金额 | | :--: | :------------------: | :------: | @@ -132,3 +121,31 @@ | 8 | 真心不过半斤ぴ | 5 | | 9 | … | … | + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Footer.vue b/components/Footer.vue new file mode 100644 index 0000000..fab8bb7 --- /dev/null +++ b/components/Footer.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/components/Header.vue b/components/Header.vue new file mode 100644 index 0000000..6f9778f --- /dev/null +++ b/components/Header.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/components/Main.vue b/components/Main.vue new file mode 100644 index 0000000..0f9bbaa --- /dev/null +++ b/components/Main.vue @@ -0,0 +1,301 @@ + + + + + \ No newline at end of file diff --git a/entrypoints/background.ts b/entrypoints/background.ts new file mode 100644 index 0000000..e7754cb --- /dev/null +++ b/entrypoints/background.ts @@ -0,0 +1,24 @@ +import {translator} from "./translator/translator"; +import {Config} from "./utils/model"; + +export default defineBackground(async () => { + + // 1、获得配置并监控变化 + let config: Config = new Config(); + await storage.getItem('local:config').then((value) => { + if (typeof value === 'string' && value) config = JSON.parse(value); + }); + storage.watch('local:config', (newValue, oldValue) => { + if (typeof newValue === 'string' && newValue) config = JSON.parse(newValue); + }); + + // 2、监听来自 main 的消息 + browser.runtime.onMessage.addListener(message => { + return new Promise((resolve, reject) => { + // 翻译 + translator[config.service](config, message) + .then(resp => resolve(resp)) // 成功 + .catch(error => reject(error)); // 失败 + }); + }); +}); \ No newline at end of file diff --git a/entrypoints/compat/compat.ts b/entrypoints/compat/compat.ts new file mode 100644 index 0000000..51b6b8c --- /dev/null +++ b/entrypoints/compat/compat.ts @@ -0,0 +1,41 @@ +// 兼容部分网站独特的 DOM 结构 + +type ReplaceFunction = (node: any, text: any) => any; +type SelectFunction = (url: any) => any; + +interface ReplaceCompatFn { + [domain: string]: ReplaceFunction; +} +interface SelectCompatFn { + [domain: string]: SelectFunction; +} + +// 根据浏览器 url.host 是获取获取主域名 +export function getMainDomain(url: any) { + const parts = url.split('.').reverse(); + return `${parts[1]}.${parts[0]}` +} + +// 文本替换环节的兼容函数,主域名 : 兼容函数 +export const replaceCompatFn: ReplaceCompatFn = { + ["youtube.com"]: (node: any, text: any) => { + // 替换 innerText + let temp = document.createElement('span'); + temp.innerHTML = text; + node.innerHTML = temp.innerText; + return node.outerHTML; + }, +}; + +// 元素 node 选择环节的兼容函数 +export const selectCompatFn: SelectCompatFn = { + ["mvnrepository.com"]: (node: any) => { + if (node.tagName.toLowerCase() === 'div' && node.classList.contains('im-description')) return true + }, + ["www.aozora.gr.jp"]: (node: any) => { + if (node.tagName.toLowerCase() === 'div' && node.classList.contains('main_text')) return true + }, + ["youtube.com"]: (node: any) => { + if (node.tagName.toLowerCase() === 'yt-formatted-string') return true + } +} \ No newline at end of file diff --git a/entrypoints/content.ts b/entrypoints/content.ts new file mode 100644 index 0000000..3780c2e --- /dev/null +++ b/entrypoints/content.ts @@ -0,0 +1,65 @@ +import {Config} from "./utils/model"; +import {cssInject} from "./main/css"; +import {handler} from "./main/dom"; +import {cache} from "./utils/cache"; + +export default defineContentScript({ + matches: [''], // 匹配所有页面 + runAt: 'document_end', // 在页面加载完成后运行 + async main() { + + cssInject(); // css 样式注入 + cache.cleaner(); // 检测是否清理缓存 + + // 获得配置并监控变化 + let config: Config = new Config(); + await storage.getItem('local:config').then((value) => { + if (typeof value === 'string' && value) Object.assign(config, JSON.parse(value)); + }); + storage.watch('local:config', (newValue, oldValue) => { + if (typeof newValue === 'string' && newValue) Object.assign(config, JSON.parse(newValue)); + }); + + // 鼠标移动事件监听 + const screen = {mouseX: 0, mouseY: 0, hotkeyPressed: false} + // 1、失去焦点时 hotkeyPressed = false + window.addEventListener('blur', () => screen.hotkeyPressed = false) + // 2、抬起快捷按键时 hotkeyPressed = false + window.addEventListener('keyup', event => { + if (config.hotkey === event.key) screen.hotkeyPressed = false; + }) + // 3、按下快捷按键时 hotkeyPressed = true 并翻译节点 + window.addEventListener('keydown', event => { + if (config.hotkey === event.key) { + screen.hotkeyPressed = true; + handler(config, screen.mouseX, screen.mouseY) + } + }) + + // 4、鼠标移动时更新位置,并根据 hotkeyPressed 决定是否触发翻译 + document.body.addEventListener('mousemove', event => { + screen.mouseX = event.clientX; + screen.mouseY = event.clientY; + if (screen.hotkeyPressed) { + handler(config, screen.mouseX, screen.mouseY, 25) + } + }); + // 5、(手机端)触摸事件,三指触摸时触发翻译(取触摸点中心位置) + document.body.addEventListener('touchstart', event => { + if (event.touches.length === 3 && screen.hotkeyPressed) { + let centerX = (event.touches[0].clientX + event.touches[1].clientX + event.touches[2].clientX) / 3; + let centerY = (event.touches[0].clientY + event.touches[1].clientY + event.touches[2].clientY) / 3; + handler(config, centerX, centerY) + } + }); + + // background.ts + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.message === 'clearCache') { + cache.clearCurrentHostCache() + } + sendResponse(); + return true; + }); + } +}) diff --git a/entrypoints/main/check.ts b/entrypoints/main/check.ts new file mode 100644 index 0000000..854594d --- /dev/null +++ b/entrypoints/main/check.ts @@ -0,0 +1,56 @@ +import {customModelString, services} from "../utils/option"; +import {Config} from "../utils/model"; +import {sendErrorMessage} from "../utils/tip"; + +// 检查翻译前配置 +export function checkConfig(config: Config): boolean { + // 1、检查插件是否开启 + if (!config.on) return false; + + // 2、检查 token 是否已经输入(如果是需要 token 的服务且 token 为空时返回 false) + if ((services.isUseToken(config.service) && !config.token[config.service]) || (config.service === services.yiyan && (!config.ak || !config.sk))) { + sendErrorMessage("令牌尚未配置,请前往设置页配置") + return false; + } + + // 3、检查模型 model 是否已经选择(如果是 AI 且模型栏为空时返回 false) + if (services.isAI(config.service) && + (!config.model[config.service] || + (config.model[config.service] === customModelString && config.customModel[config.service] === ""))) { + sendErrorMessage("模型尚未配置,请前往设置页配置") + return false; + } + + return true; +} + + +// 检查节点是否需要翻译 +export function skipNode(node: Node): boolean { + // 如果符合条件,则跳过 + return !node || !node.textContent || node.textContent.trim() === "" || hasLoadingSpinner(node) || hasRetryTag(node) +} + +// 判断是否有加载动画 +export function hasLoadingSpinner(node: any) { + // 文本节点的下一个节点是否包含加载动画 + if (node.nodeType === Node.TEXT_NODE ) return false; + + if (node.classList.contains('fluent-read-loading')) return true; + for (let child of node.children) { + if (hasLoadingSpinner(child)) return true; + } + return false; +} + +// 判断节点是否有重试属性 +export function hasRetryTag(node: any) { + // 文本节点的下一个节点是否包含重试属性 + if (node.nodeType === Node.TEXT_NODE ) return false; + + if (node.classList.contains('fluent-read-failure')) return true; + for (let child of node.children) { + if (hasLoadingSpinner(child)) return true; + } + return false; +} \ No newline at end of file diff --git a/entrypoints/main/common.ts b/entrypoints/main/common.ts new file mode 100644 index 0000000..c48cb37 --- /dev/null +++ b/entrypoints/main/common.ts @@ -0,0 +1,39 @@ +// 面向切面编程,后置函数编写 +import {html} from 'js-beautify'; + +// 格式化翻译后文本 +export function beautyHTML(text: string): string { + // 1、js-beautify格式化代码 + // 2、正则表达式匹配 < a> 则将其替换为 (因为 js-beautify 只能将 < a> 格式化为 < a>) + // return html(text).replace(/<\s+/g, "<") + text = replaceSensitiveWords(text); + + return html(text) +} + +// 替换 svg 标签中的一些大小写敏感的词(html 不区分大小写,但 svg 标签区分大小写) +function replaceSensitiveWords(text: string): string { + return text.replace(/viewbox|preserveaspectratio|clippathunits|gradienttransform|patterncontentunits|lineargradient|clippath/gi, (match) => { + switch (match.toLowerCase()) { + case 'viewbox': + return 'viewBox'; + case 'preserveaspectratio': + return 'preserveAspectRatio'; + case 'clippathunits': + return 'clipPathUnits'; + case 'gradienttransform': + return 'gradientTransform'; + case 'patterncontentunits': + return 'patternContentUnits'; + case 'lineargradient': + return 'linearGradient'; + case 'clippath': + return 'clipPath'; + default: + return match; + } + }); +} + + + diff --git a/entrypoints/main/css.ts b/entrypoints/main/css.ts new file mode 100644 index 0000000..89ad1cc --- /dev/null +++ b/entrypoints/main/css.ts @@ -0,0 +1,34 @@ +const css = ` +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} +.fluent-read-loading { + border: 3px solid #f3f3f3; /* 轨迹灰色 */ + border-top: 3px solid blue; /* 主色调,首次翻译蓝色、缓存应改为绿色 */ + border-radius: 50%; + width: 12px; + height: 12px; + animation: spin 1s linear infinite; + display: inline-block; +} + +.fluent-read-retry-wrapper { + display: inline-flex; + align-items: center; +} +.fluent-read-retry, .fluent-read-reason { + color: #428ADF; + text-decoration: underline; + text-underline-offset: 0.2em; + margin-left: 0.2em; + font-size: 1em; + cursor: pointer; +} +`; + +export function cssInject() { + const style = document.createElement('style'); + style.textContent = css; + document.head.appendChild(style); +} diff --git a/entrypoints/main/detectlang.ts b/entrypoints/main/detectlang.ts new file mode 100644 index 0000000..79e272f --- /dev/null +++ b/entrypoints/main/detectlang.ts @@ -0,0 +1,13 @@ +import {franc} from "franc-min"; + +// 输出标准的语言类型,franc 只返回最可信的结果,francAll 返回所有结果并包含确信度 +export function detectlang(origin: any): string { + let find = franc(origin, {minLength: 0}); + if (find === "cmn") return "zh-Hans" + else if (find === "eng") return "en" + else if (find === "jpn") return "ja" + else if (find === "kor") return "ko" + else if (find === "fra") return "fr" + else if (find === "rus") return "ru" + else return find +} \ No newline at end of file diff --git a/entrypoints/main/dom.ts b/entrypoints/main/dom.ts new file mode 100644 index 0000000..c834239 --- /dev/null +++ b/entrypoints/main/dom.ts @@ -0,0 +1,276 @@ +import {checkConfig, skipNode} from "./check"; +import {Config} from "../utils/model"; +import {getMainDomain, replaceCompatFn, selectCompatFn} from "../compat/compat"; +import {cache} from "../utils/cache"; +import {detectlang} from "./detectlang"; +import {services} from "../utils/option"; +import {insertFailedTip, insertLoadingSpinner} from "./icon"; +import {beautyHTML} from "./common"; +import {throttle} from "@/entrypoints/utils/tip"; + +let hoverTimer: any; // 鼠标悬停计时器 +let outerHTMLSet = new Set(); // 去重 +const url = new URL(location.href.split('?')[0]); + +// 当遇到这些 tag 时直接翻译 +const directSet = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', "li"]); +// 当遇到这些节点时,不应当翻译 +const skipSet = new Set(["html", "body",]); // "iframe" 暂时去除 +// button兼容(避免按钮失去响应式点击事件) +const buttonCompatSet = new Set(['button', 'submit', 'reset', 'image', 'file']); + +// 1、若某节点的兄弟节点中「只」包含下列节点,则应当翻译其父节点 +// 2、llm 模式翻译时,翻译下列节点的 outerHTML 属性 +const chileSet = new Set([ + 'a', 'b', 'strong', 'span', 'img', 'br', 'em', 'u', 'small', 'sub', 'sup', 'i', 'font', + 'big', 'strike', 's', 'del', 'ins', 'mark', 'cite', 'q', 'abbr', 'acronym', 'dfn', 'svg', + 'code', 'samp', 'kbd', 'var', 'pre', 'address', 'time', 'ruby', 'rb', 'rt', 'rp', 'bdi', 'bdo', 'wbr', + 'details', 'summary', 'menuitem', 'menu', 'dialog', 'slot', 'template', 'shadow', 'content', 'element', +]); + +export function handler(config: Config, mouseX: number, mouseY: number, time: number = 0) { + + // 检查配置项是否正确 + if (!checkConfig(config)) return; + + clearTimeout(hoverTimer); // 清除计时器 + hoverTimer = setTimeout(() => { + // 获取起始节点 + let node = grabNode(config, document.elementFromPoint(mouseX, mouseY)); // 获取最终需要翻译的节点 + + // 跳过不需要翻译的节点 + if (skipNode(node)) return; + + // 去重判断 + let outerHTMLTemp = node.outerHTML; + if (outerHTMLSet.has(outerHTMLTemp)) { + // console.log('重复节点', node); + return; + } + outerHTMLSet.add(outerHTMLTemp); + + // 检测缓存 + let outerHTMLCache = cache.get(config, node.outerHTML); + if (outerHTMLCache) { + // console.log("缓存命中:", outerHTMLCache); + let spinner = insertLoadingSpinner(node, true); + setTimeout(() => { // 延迟 remove 转圈动画与替换文本 + spinner.remove(); + outerHTMLSet.delete(outerHTMLTemp); + let compatFn = replaceCompatFn[getMainDomain(url.host)]; // 兼容函数 + if (compatFn) { + compatFn(node, outerHTMLCache); // 兼容函数 + } else { + node.outerHTML = outerHTMLCache; + } + deferCacheRemoval(outerHTMLCache); + }, 250); + return; + } + // console.log("无缓存:", node.outerHTML); + // 无缓存,正常翻译 + translate(config, node); + }, time); +} + +// 按钮翻译节流 +const btnTransThrottle = throttle(btnTrans, 250); + +function btnTrans(config: Config, child: any) { + if (services.isMachine(config.service)) { + // 机器翻译 innerHTML + let rs = cache.get(config, child.innerHTML); + if (rs) { + child.innerHTML = rs; + return; + } + // background.ts + browser.runtime.sendMessage({context: document.title, origin: child.innerHTML}) + .then((text: string) => { + cache.set(config, child.innerHTML, text) + child.innerHTML = text + }).catch(error => console.error('调用失败:', error)) + } else { + // LLM 翻译 textContent + let rs = cache.get(config, child.textContent); + if (rs) { + child.textContent = rs; + return; + } + // background.ts + browser.runtime.sendMessage({context: document.title, origin: child.textContent}) + .then((text: string) => { + cache.set(config, child.textContent, text) + child.textContent = text + }).catch(error => console.error('调用失败:', error)) + } +} + +// 返回最终应该翻译的父节点或 false +function grabNode(config: Config, node: any): any { + let curTag = node.tagName.toLowerCase(); // 当前 tag + + // 1、全局节点与空节点、input 节点、文字过多的节点、class="notranslate" 的节点不翻译 + if (!node || skipSet.has(curTag) || curTag === 'input' || node.classList.contains('notranslate') || node.textContent.length > 8192) { + return false; + } + + // 2、普通适配,遇到这些标签则直接翻译节点 + if (directSet.has(curTag)) return node; + + // 3、button 按钮适配 + if (curTag === 'button' + || (curTag === 'span' && node.parentNode && node.parentNode.tagName.toLowerCase() === 'button')) { + // 翻译按钮内部而不是整个按钮,避免按钮失去响应式点击事件 + if (node.textContent.trim() !== '') { + btnTransThrottle(config, node) + } + + return false; + } + + // 4、特殊适配,根据域名进行特殊处理 + let fn = selectCompatFn[getMainDomain(url.host)]; + if (fn && fn(node)) return node; + + // 4、如果遇到 span,则首先该节点就符合翻译条件 + if (curTag === "span" || node.nodeType === Node.TEXT_NODE || detectChildMeta(node)) { + return grabNode(config, node.parentNode) || node; // 递归向上寻找最后一个符合的父节点 + } + + // 5、如果节点是div并且不符合一般翻译条件,可翻译首行文本 + if (curTag === 'div' || curTag === 'label') { + // 遍历子节点,寻找首个文本节点或 a 标签节点 + let child = node.firstChild; + while (child) { + if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== '') { + // background.ts + browser.runtime.sendMessage({context: document.title, origin: child.textContent}) + .then((text: string) => child.textContent = text) + .catch(error => console.error('调用失败:', error)) + + return false; // 只翻译首行文本,不再进行后续步骤 + } + child = child.nextSibling; + } + } + + // console.log('不翻译节点:', node); + + return false +} + +// failCount 用于记录翻译失败次数,默认为 0 +export function translate(config: Config, node: any) { + + // console.log("翻译节点:", node) + + // 正则表达式去除所有空格后再检查语言类型 + // 如果源语言与目标语言相同,则不翻译 + if (detectlang(node.textContent.replace(/[\s\u3000]/g, '')) === config.to) return; + + // origin 是待翻译文本;机器翻译 origin = outerHTML;LLM 翻译 origin = llmGetText(node) + let origin = services.isMachine(config.service) ? node.outerHTML : llmGetText(node); + + let spinner = insertLoadingSpinner(node); // 插入转圈动画 + let timeout = setTimeout(() => { + insertFailedTip(config, node, "timeout", spinner); + }, 45000); + + config.count++; // 翻译次数 +1 + storage.setItem('local:config', JSON.stringify(config)); // 更新配置 + + // 调用翻译服务(正在翻译ing...),允许失败重试 3 次、间隔 500ms + const translating = (failCount = 0) => { + browser.runtime.sendMessage({context: document.title, origin: origin}) + .then((text: string) => { + clearTimeout(timeout) // 取消超时 + spinner.remove() // 移除 spinner + + // 格式化 html 翻译结果 + text = beautyHTML(text) + + // console.log("翻译前的句子:", origin); + // console.log("翻译后的句子:", text); + + if (!text || origin === text) return; + + let oldOuterHtml = node.outerHTML // 保存旧的 outerHTML + + let newOuterHtml = text + if (services.isMachine(config.service)) { + // 1、机器翻译 + if (!node.parentNode) return; + let compatFn = replaceCompatFn[getMainDomain(url.host)]; + if (compatFn) { + oldOuterHtml = node.outerHTML; + compatFn(node, text); + newOuterHtml = node.outerHTML; + } else { + node.outerHTML = text; + } + } else { + // 2、LLM 翻译 + node.innerHTML = text; + newOuterHtml = node.outerHTML; + } + + cache.set(config, oldOuterHtml, newOuterHtml); // 设置缓存 + // 延迟删除 newOuterHtml,立即删除 oldOuterHtml + deferCacheRemoval(newOuterHtml); + outerHTMLSet.delete(oldOuterHtml); + }) + .catch(error => { + clearTimeout(timeout); + if (failCount < 3) { // 如果失败次数小于3次,重新尝试 + // 延迟 500ms 后重试 + setTimeout(() => { + translating(failCount + 1); + }, 500); + } else { // 达到3次失败后,显示失败提示 + insertFailedTip(config, node, error.toString() || "", spinner); + } + }); + } + + translating(); // 开始翻译 +} + +// 检测子元素中是否包含指定标签以外的元素 +function detectChildMeta(parent: any): boolean { + let child = parent.firstChild; + while (child) { + if (child.nodeType === Node.ELEMENT_NODE && !chileSet.has(child.nodeName.toLowerCase())) { + return false; + } + child = child.nextSibling; + } + return true; +} + +// 延迟删除缓存,避免短时间内重复翻译 +function deferCacheRemoval(key: any, time = 250) { + outerHTMLSet.add(key); + setTimeout(() => { + outerHTMLSet.delete(key); + }, time); +} + +// LLM 模式获取翻译文本 +function llmGetText(node: any) { + let text = ""; + // 遍历所有子节点 + node.childNodes.forEach((child: any) => { + if (child.nodeType === Node.TEXT_NODE) { + text += child.nodeValue; // 文本节点,直接添加至 text + } else if (child.nodeType === Node.ELEMENT_NODE) { + // 检查是否为特定节点 + if (chileSet.has(child.tagName.toLowerCase())) { + text += child.outerHTML; + } else { + text += llmGetText(child); // 递归 + } + } + }); + return text; +} diff --git a/entrypoints/main/icon.ts b/entrypoints/main/icon.ts new file mode 100644 index 0000000..d2039ce --- /dev/null +++ b/entrypoints/main/icon.ts @@ -0,0 +1,78 @@ +// 失败时展示的图标 +import {sendErrorMessage} from "../utils/tip"; +import {Config} from "../utils/model"; +import 'element-plus/es/components/message/style/css' +import {translate} from "./dom"; + +const icon = { + retry: ` + +`, + warn: ` + +` +} + + +export function insertFailedTip(config: Config, node: any, errMsg: string, spinner: HTMLElement) { + spinner?.remove(); // 取消转圈动画 + + // 创建包装元素 + const wrapper = document.createElement('span'); + wrapper.classList.add('fluent-read-retry-wrapper'); + + // 创建重试按钮 + const retryBtn = document.createElement('span'); + retryBtn.innerText = '重试'; + retryBtn.classList.add('fluent-read-retry'); + retryBtn.addEventListener('click', function () { + wrapper.remove(); // 移除错误提示元素,重新翻译 + translate(config, node); + }); + + // 添加失败标记 failure + node.classList.add('fluent-read-failure'); + + // 创建错误信息提示按钮 + const errorTip = document.createElement('span'); + errorTip.innerText = '错误原因'; + errorTip.classList.add('fluent-read-reason'); + errorTip.addEventListener('click', function () { + if (errMsg.includes("auth failed") || errMsg.includes("API key") || errMsg.includes("api key")) { + sendErrorMessage("token 令牌设置错误,请前往设置页检查") + } else if (errMsg.includes("quota") || errMsg.includes("limit")) { + sendErrorMessage("翻译次数已达上限,请稍后再试") + } else if (errMsg.includes("network error")) { + sendErrorMessage("网络错误,请检查网络连接") + } else if (errMsg.includes("model")) { + sendErrorMessage("model 模型设置错误,请前往设置页检查") + } else if (errMsg.includes("timeout")) { + sendErrorMessage("请求超时,请稍后再试") + } else { + sendErrorMessage(errMsg || "未知错误,请联系开发者") + } + }); + + const retryElement = document.createElement('div'); + retryElement.innerHTML = icon.retry; + + const warnElement = document.createElement('div'); + warnElement.innerHTML = icon.warn; + + // 将 SVG 图标和文本添加到包装元素 + wrapper.appendChild(retryElement); // 这里不是 httpElement,而是字符串 + wrapper.appendChild(retryBtn); + wrapper.appendChild(warnElement); + wrapper.appendChild(errorTip); + + node.appendChild(wrapper); +} + +// 插入加载动画 +export function insertLoadingSpinner(node: any, isCache: boolean = false) { + const spinner = document.createElement('span'); + spinner.className = 'fluent-read-loading'; + if (isCache) spinner.style.borderTop = '3px solid green' // 存在缓存时改为绿色 + node.appendChild(spinner); + return spinner; +} diff --git a/entrypoints/popup/App.vue b/entrypoints/popup/App.vue new file mode 100644 index 0000000..79cd091 --- /dev/null +++ b/entrypoints/popup/App.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/entrypoints/popup/index.html b/entrypoints/popup/index.html new file mode 100644 index 0000000..fde047d --- /dev/null +++ b/entrypoints/popup/index.html @@ -0,0 +1,13 @@ + + + + + + 流畅阅读 + + + +
+ + + diff --git a/entrypoints/popup/main.ts b/entrypoints/popup/main.ts new file mode 100644 index 0000000..11d6695 --- /dev/null +++ b/entrypoints/popup/main.ts @@ -0,0 +1,15 @@ +import {createApp} from 'vue'; +import './style.css'; +import App from './App.vue'; +import ElementPlus, {ElMessage} from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' + + +const app = createApp(App); +app.use(ElementPlus) // 导入 element-plus ui +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { // 注册所有 element-plus 的图标 + app.component(key, component) +} + +app.mount('#app'); diff --git a/entrypoints/popup/style.css b/entrypoints/popup/style.css new file mode 100644 index 0000000..b58a21b --- /dev/null +++ b/entrypoints/popup/style.css @@ -0,0 +1,63 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.lightblue { + /*background-color: #f0f2f5;*/ +} + +.rounded-corner { + border-radius: 5px; /* 或者您期望的圆角大小 */ +} + +/*popup选项框文字*/ +.popup-text { + font-size: 15px; + color: #000000; +} + +.popup-vertical-left { + display: flex; /* 启用 Flexbox */ + align-items: center; /* 垂直居中 */ + justify-content: left; /* 水平居中,如果需要的话 */ + height: 100%; /* 父容器等高 */ +} + +.margin-left-2em { + margin-left: 2em; +} + +.margin-bottom { + margin-bottom: 10px; +} + +.custom-padding { + padding: 10px !important; +} + +#app { + min-width: 340px; + /*max-width: 1280px;*/ + margin: 0 auto; + padding: 0; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } +} diff --git a/entrypoints/translator/claude.ts b/entrypoints/translator/claude.ts new file mode 100644 index 0000000..dc1aa14 --- /dev/null +++ b/entrypoints/translator/claude.ts @@ -0,0 +1,31 @@ +import {Config} from "../utils/model"; +import {services} from "../utils/option"; +import {method, urls} from "../utils/constant"; +import {claudeMsgTemplate} from "../utils/template"; + +async function claude(config: Config, message: any) { + // 构建请求头 + let headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('x-api-key', config.token[services.claude]); + headers.append('anthropic-version', '2023-06-01'); + + // 判断是否使用代理 + let url: string = config.proxy[config.service] ? config.proxy[config.service] : urls[services.claude] + + // 发起 fetch 请求 + const resp = await fetch(url, { + method: method.POST, + headers: headers, + body: claudeMsgTemplate(config, message.origin) + }) + if (resp.ok) { + let result = await resp.json(); + return result.content[0].text; + } else { + console.log(resp) + throw new Error(`请求失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default claude; diff --git a/entrypoints/translator/deepl.ts b/entrypoints/translator/deepl.ts new file mode 100644 index 0000000..0322515 --- /dev/null +++ b/entrypoints/translator/deepl.ts @@ -0,0 +1,34 @@ +import {Config} from "../utils/model"; +import {method, urls} from "../utils/constant"; +import {services} from "../utils/option"; + +async function deepl(config: Config, message: any) { + // deepl 不支持 zh-Hans,需要转换为 zh + let targetLang = config.to === 'zh-Hans' ? 'zh' : config.to; + + const resp = await fetch(urls[services.deepL], { + method: method.POST, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'DeepL-Auth-Key ' + config.token[services.deepL] + }, + body: JSON.stringify({ + text: [message.origin], + target_lang: targetLang, + tag_handling: 'html', + context: message.context, // 添加上下文辅助信息 + preserve_formatting: true + }) + }); + + if (resp.ok) { + let result = await resp.json(); + return result.translations[0].text + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} 请检查 token 是否正确`); + } +} + +export default deepl; + diff --git a/entrypoints/translator/gemini.ts b/entrypoints/translator/gemini.ts new file mode 100644 index 0000000..add14f8 --- /dev/null +++ b/entrypoints/translator/gemini.ts @@ -0,0 +1,26 @@ +import {Config} from "../utils/model"; +import {method} from "../utils/constant"; +import {geminiMsgTemplate} from "../utils/template"; +import {customModelString, services} from "../utils/option"; + + +async function gemini(config: Config, message: any) { + + let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] + + let url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${config.token[services.gemini]}`; + const resp = await fetch(url, { + method: method.POST, + headers: {'Content-Type': 'application/json'}, + body: geminiMsgTemplate(config, message.origin), + }); + if (resp.ok) { + let result = await resp.json(); + return result.candidates[0].content.parts[0].text; + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default gemini; diff --git a/entrypoints/translator/google.ts b/entrypoints/translator/google.ts new file mode 100644 index 0000000..292bd51 --- /dev/null +++ b/entrypoints/translator/google.ts @@ -0,0 +1,26 @@ +import {Config} from "../utils/model"; +import {method} from "../utils/constant"; + +async function google(config: Config, message: any) { + let params: any = { + client: 'gtx', sl: config.from, tl: config.to, dt: 't', strip: 1, nonced: 1, + 'q': encodeURIComponent(message.origin), + }; + let queryString = Object.keys(params).map((key: string) => key + '=' + params[key]).join('&'); + + const resp = await fetch('https://translate.googleapis.com/translate_a/single?' + queryString, { + method: method.GET, + }); + + if (resp.ok) { + let result = await resp.json(); + let sentence = ''; + result[0].forEach((e: any) => sentence += e[0]); + return sentence; + } else { + console.log(resp); + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default google; \ No newline at end of file diff --git a/entrypoints/translator/infini.ts b/entrypoints/translator/infini.ts new file mode 100644 index 0000000..5d96dbb --- /dev/null +++ b/entrypoints/translator/infini.ts @@ -0,0 +1,31 @@ +// 引入所需模块 +import {Config} from "../utils/model"; +import {customModelString, services} from "../utils/option"; +import {method} from "../utils/constant"; +import {openaiMsgTemplate} from "@/entrypoints/utils/template"; + +async function infini(config: Config, message: any) { + // 构建请求头 + let headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('Authorization', `Bearer ${config.token[services.infini]}`); + + let model = config.model[services.infini] === customModelString ? config.customModel[services.infini] : config.model[services.infini] + + // 发起 fetch 请求 + const resp = await fetch(`https://cloud.infini-ai.com/maas/${model}/nvidia/chat/completions`, { + method: method.POST, + headers: headers, + body: openaiMsgTemplate(config, message.origin) + }); + + if (resp.ok) { + let result = await resp.json(); + return result.choices[0].message.content + } else { + console.error(resp); + throw new Error(`请求失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default infini; diff --git a/entrypoints/translator/microsoft.ts b/entrypoints/translator/microsoft.ts new file mode 100644 index 0000000..e96921d --- /dev/null +++ b/entrypoints/translator/microsoft.ts @@ -0,0 +1,53 @@ +import {Config} from "../utils/model"; +import {services} from "../utils/option"; + +async function microsoft(config: Config, message: any) { + let fromLang = config.from === 'auto' ? '' : config.from; + + const jwtToken = await refreshToken(config.token[services.microsoft]); + const resp = await fetch(`https://api-edge.cognitive.microsofttranslator.com/translate?from=${fromLang}&to=${config.to}&api-version=3.0&includeSentenceLength=true&textType=html`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Ocp-Apim-Subscription-Key': config.token[services.microsoft], + 'Authorization': 'Bearer ' + jwtToken + }, + body: JSON.stringify([{Text: message.origin}]) + }); + + if (resp.ok) { + let result = await resp.json(); + return result[0].translations[0].text; + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +async function refreshToken(token: string) { + const decodedToken = parseJwt(token); + const currentTimestamp = Math.floor(Date.now() / 1000); // 当前时间的UNIX时间戳(秒) + if (decodedToken && currentTimestamp < decodedToken.exp) { + return token; + } + // 如果令牌无效或已过期,则尝试获取新令牌 + const resp = await fetch("https://edge.microsoft.com/translate/auth") + if (resp.ok) return resp.text(); + else throw new Error(`请求失败: ${resp}`); +} + +// 解析 jwt,返回解析后对象 +function parseJwt(token: string) { + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + return JSON.parse(jsonPayload); + } catch (e) { + return null; + } +} + +export default microsoft; \ No newline at end of file diff --git a/entrypoints/translator/moonshot.ts b/entrypoints/translator/moonshot.ts new file mode 100644 index 0000000..2202038 --- /dev/null +++ b/entrypoints/translator/moonshot.ts @@ -0,0 +1,28 @@ +import {Config} from "../utils/model"; +import {services} from "../utils/option"; +import {method, urls} from "../utils/constant"; +import {openaiMsgTemplate} from "../utils/template"; + +async function moonshot(config: Config, message: any) { + let token = config.token[services.moonshot]; + // 构建请求头 + let headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('Authorization', `Bearer ${token}`); + + // 发起 fetch 请求 + const resp = await fetch(urls[services.moonshot], { + method: method.POST, + headers: headers, + body: openaiMsgTemplate(config, message.origin) + }); + if (resp.ok) { + let result = await resp.json(); + return result.choices[0].message.content + } else { + console.log(resp); + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default moonshot; diff --git a/entrypoints/translator/ollama.ts b/entrypoints/translator/ollama.ts new file mode 100644 index 0000000..3670faa --- /dev/null +++ b/entrypoints/translator/ollama.ts @@ -0,0 +1,21 @@ +import {Config} from "../utils/model"; +import {ollamaMsgTemplate} from "../utils/template"; +import {method} from "../utils/constant"; + +async function ollama(config: Config, message: any) { + const resp = await fetch(config.native, { + method: method.POST, + headers: {'Content-Type': 'application/json'}, + body: ollamaMsgTemplate(config, message.origin) + }); + + if (resp.ok) { + let result = await resp.json(); + return result.choices[0].message.content + } else { + console.log("翻译失败:", resp); + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default ollama; \ No newline at end of file diff --git a/entrypoints/translator/openai.ts b/entrypoints/translator/openai.ts new file mode 100644 index 0000000..69e7385 --- /dev/null +++ b/entrypoints/translator/openai.ts @@ -0,0 +1,31 @@ +import {Config} from "../utils/model"; +import {services} from "../utils/option"; +import {method, urls} from "../utils/constant"; +import {openaiMsgTemplate} from "../utils/template"; + +async function openai(config: Config, message: any) { + // 构建请求头 + let headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('Authorization', `Bearer ${config.token[services.openai]}`); + + // 判断是否使用代理 + let url: string = config.proxy[config.service] ? config.proxy[config.service] : urls[services.openai] + + // 发起 fetch 请求 + const resp = await fetch(url, { + method: method.POST, + headers: headers, + body: openaiMsgTemplate(config, message.origin) + }) + if (resp.ok) { + let result = await resp.json(); + return result.choices[0].message.content + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + + +export default openai; \ No newline at end of file diff --git a/entrypoints/translator/tongyi.ts b/entrypoints/translator/tongyi.ts new file mode 100644 index 0000000..76b1438 --- /dev/null +++ b/entrypoints/translator/tongyi.ts @@ -0,0 +1,27 @@ +import {Config} from "../utils/model"; +import {services} from "../utils/option"; +import {method, urls} from "../utils/constant"; +import {tongyiMsgTemplate} from "../utils/template"; + +// 文档:https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-thousand-questions-metering-and-billing +async function tongyi(config: Config, message: any) { + // 构建请求头 + let headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('Authorization', `Bearer ${config.token[services.tongyi]}`); + const resp = await fetch(urls[services.tongyi], { + method: method.POST, + headers: headers, + body: tongyiMsgTemplate(config, message.origin) + }); + + if (resp.ok) { + let result = await resp.json(); + return result.output.text + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default tongyi; diff --git a/entrypoints/translator/translator.ts b/entrypoints/translator/translator.ts new file mode 100644 index 0000000..354b66e --- /dev/null +++ b/entrypoints/translator/translator.ts @@ -0,0 +1,31 @@ +import {services} from "../utils/option"; +import microsoft from "./microsoft"; +import deepl from "./deepl"; +import openai from "./openai"; +import moonshot from "./moonshot"; +import ollama from "./ollama"; +import tongyi from "./tongyi"; +import zhipu from "./zhipu"; +import yiyan from "./yiyan"; +import gemini from "./gemini"; +import google from "./google"; +import xiaoniu from "./xiaoniu"; +import claude from "./claude"; +import infini from "@/entrypoints/translator/infini"; + + +export const translator = { + [services.microsoft]: microsoft, + [services.deepL]: deepl, + [services.google]: google, + [services.openai]: openai, + [services.moonshot]: moonshot, + [services.ollama]: ollama, + [services.tongyi]: tongyi, + [services.zhipu]: zhipu, + [services.yiyan]: yiyan, + [services.gemini]: gemini, + [services.xiaoniu]: xiaoniu, + [services.claude]: claude, + [services.infini]: infini, +} diff --git a/entrypoints/translator/xiaoniu.ts b/entrypoints/translator/xiaoniu.ts new file mode 100644 index 0000000..63f5628 --- /dev/null +++ b/entrypoints/translator/xiaoniu.ts @@ -0,0 +1,24 @@ +import {Config} from "../utils/model"; +import {method, urls} from "../utils/constant"; +import {services} from "../utils/option"; + +async function xiaoniu(config: Config, message: any) { + // 根据需要调整目标语言 + let targetLang = config.to === 'zh-Hans' ? 'zh' : config.to; + + const resp = await fetch(urls[services.xiaoniu], { + method: method.POST, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: `from=auto&to=${targetLang}&apikey=${config.token[services.xiaoniu]}&src_text=${encodeURIComponent(message.origin)}` + }); + + if (resp.ok) { + let result = await resp.json(); + return result.tgt_text + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +export default xiaoniu; diff --git a/entrypoints/translator/yiyan.ts b/entrypoints/translator/yiyan.ts new file mode 100644 index 0000000..e6ccd9e --- /dev/null +++ b/entrypoints/translator/yiyan.ts @@ -0,0 +1,68 @@ +import {services} from "../utils/option"; +import {Config} from "../utils/model"; +import {yiyanMsgTemplate} from "../utils/template"; +import {method} from "../utils/constant"; + +// ERNIE-Bot 4.0 模型,模型定价页面:https://console.bce.baidu.com/qianfan/chargemanage/list +// api 文档中心:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t + +// 文心一言根据 ak, sk 获取 secret 和 expiration +async function yiyan(config: Config, message: any) { + + let model = config.model[services.yiyan] + // model 参数转换 + if (model === "ERNIE-Bot 4.0") model = "completions_pro" + else if (model==="ERNIE-Bot") model="completions" + + const secret = await getSecret(config); + const url = `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${model}?access_token=${secret}`; + + // 发起 fetch 请求 + const resp = await fetch(url, { + method: method.POST, + headers: {'Content-Type': 'application/json'}, + body: yiyanMsgTemplate(config, message.origin) + }); + + if (resp.ok) { + let result = await resp.json(); + if (result.error_code) throw new Error(`翻译失败: ${result.error_code} ${result.error_msg}`) + return result.result + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +async function getSecret(config: Config) { + let secret, expiration; + config.extra[services.yiyan] && ({secret, expiration} = config.extra[services.yiyan]); + + // 检查 secret 是否存在且未过期 + if (secret && config.ak && config.sk && expiration > Date.now()) return secret; + + // 构建请求参数 + let params = new URLSearchParams({ + 'grant_type': 'client_credentials', + 'client_id': config.ak, + 'client_secret': config.sk, + }); + + // 发起 fetch 请求 + const resp = await fetch('https://aip.baidubce.com/oauth/2.0/token', { + method: method.POST, + body: params + }); + + const res = await resp.json(); + if (resp.ok && res.access_token) { + // 获取有效时间范围,有效期30天(单位秒),需 x1000 转换为毫秒 + let expiration = new Date().getTime() + res.expires_in * 1000; + // 缓存 secret 和 expiration + config.extra[services.yiyan] = {secret: res.access_token, expiration: expiration}; + storage.setItem('local:config', JSON.stringify(config)); + return res.access_token; + } else throw new Error(res.error_description || '智谱清言获取 token 失败'); +} + +export default yiyan; \ No newline at end of file diff --git a/entrypoints/translator/zhipu.ts b/entrypoints/translator/zhipu.ts new file mode 100644 index 0000000..4939b25 --- /dev/null +++ b/entrypoints/translator/zhipu.ts @@ -0,0 +1,73 @@ +import {Config} from "../utils/model"; +import {method, urls} from "../utils/constant"; +import {services} from "../utils/option"; +import {openaiMsgTemplate} from "../utils/template"; +import CryptoJS from 'crypto-js'; + + +// 文档参考:https://open.bigmodel.cn/dev/api#nosdk +async function zhipu(config: Config, message: any) { + // 智谱根据 token 获取 secret(签名密钥) 和 expiration + let token = config.token[services.zhipu]; + let secret, expiration; + config.extra[services.zhipu] && ({secret, expiration} = config.extra[services.zhipu]); + if (!secret || expiration <= Date.now()) { + secret = generateToken(token); + if (!secret) throw new Error('无法生成令牌'); + // 保存 secret 和 expiration + config.extra[services.zhipu] = {secret, expiration: Date.now() + 3600000 * 24}; + storage.setItem('local:config', JSON.stringify(config)); + } + + // 构建请求头 + let headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('Authorization', `Bearer ${secret}`); + + // 发起 fetch 请求 + const resp = await fetch(urls[services.zhipu], { + method: method.POST, + headers: headers, + body: openaiMsgTemplate(config, message.origin) + }); + + if (resp.ok) { + let result = await resp.json(); + return result.choices[0].message.content; + } else { + console.log(resp) + throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); + } +} + +function generateToken(APIKey: string) { + if (!APIKey || !APIKey.includes('.')) { + console.log("API Key 格式错误:", APIKey) + return; + } + let duration = 3600000 * 24; // 生成的 token 默认24小时后过期 + const [key, secret] = APIKey.split('.'); + + return generateJWT(secret, {alg: "HS256", sign_type: "SIGN", typ: "JWT"}, { + api_key: key, + exp: Math.floor(Date.now() / 1000) + (duration / 1000), + timestamp: Math.floor(Date.now() / 1000) + }); +} + +// 生成JWT(JSON Web Token) +function generateJWT(secret: string, header: any, payload: any) { + // 对header和payload部分进行UTF-8编码,然后转换为Base64URL格式 + const encodedHeader = base64UrlSafe(btoa(JSON.stringify(header))); + const encodedPayload = base64UrlSafe(btoa(JSON.stringify(payload))); + // 生成 jwt 签名 + let hmacsha256 = base64UrlSafe(CryptoJS.HmacSHA256(encodedHeader + "." + encodedPayload, secret).toString(CryptoJS.enc.Base64)) + return `${encodedHeader}.${encodedPayload}.${hmacsha256}`; +} + +// 将Base64字符串转换为Base64URL格式的函数 +function base64UrlSafe(base64String: string) { + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +export default zhipu; diff --git a/entrypoints/utils/cache.ts b/entrypoints/utils/cache.ts new file mode 100644 index 0000000..a16e5dc --- /dev/null +++ b/entrypoints/utils/cache.ts @@ -0,0 +1,51 @@ +// localStorage +import {Config} from "./model"; +import {customModelString} from "./option"; + +const prefix = "flcache_" // fluent read cache + +// 构建缓存 key +function buildKey(config: Config, message: string) { + let service = config.service + let model = config.model[service] === customModelString ? config.customModel[service] : config.model[service] + // 前缀_服务_模型_目标语言_消息 + return prefix + service + "_" + model + "_" + config.to + "_" + message +} + +export const cache = { + set(config: Config, origin: string, result: string) { + localStorage.setItem(buildKey(config, origin), result) + localStorage.setItem(buildKey(config, result), origin) + }, + get(config: Config, origin: string) { + return localStorage.getItem(buildKey(config, origin)) + }, + remove(config: Config, origin: string) { + let result: any = localStorage.getItem(buildKey(config, origin)) + localStorage.removeItem(buildKey(config, origin)) + localStorage.removeItem(buildKey(config, result)) + }, + // 24h 清理一次缓存(每次页面打开即 main.js 时都应该调用) + cleaner() { + const lastSessionTimestamp = localStorage.getItem('flLastSessionTimestamp'); + let currentTime = new Date().getTime() + + if (!lastSessionTimestamp) { + localStorage.setItem('flLastSessionTimestamp', currentTime.toString()); + } else if (currentTime - parseInt(lastSessionTimestamp) > 24 * 3600000) { + // }else if (currentTime - parseInt(lastSessionTimestamp) > 20000) { + this.clearCurrentHostCache() + localStorage.setItem('flLastSessionTimestamp', currentTime.toString()); + } + }, + // 清除当前页面所有由 fluent read 缓存的数据 + clearCurrentHostCache() { + // 反向迭代,即使删除项也不会影响尚未迭代到的项的索引 + for (let i = localStorage.length - 1; i >= 0; i--) { + let key = localStorage.key(i); + if (key && key.startsWith("flcache_")) { + localStorage.removeItem(key); + } + } + } +} \ No newline at end of file diff --git a/entrypoints/utils/constant.ts b/entrypoints/utils/constant.ts new file mode 100644 index 0000000..92d0a13 --- /dev/null +++ b/entrypoints/utils/constant.ts @@ -0,0 +1,20 @@ +// 常量工具类 + +import {services} from "./option"; + +export const app = { + version: "0.0.2", +} + +export const urls = { + [services.deepL]: "https://api-free.deepl.com/v2/translate", + [services.openai]: "https://api.openai.com/v1/chat/completions", + [services.moonshot]: "https://api.moonshot.cn/v1/chat/completions", + [services.ollama]: "https://api.ollama.com/v1/chat/completions", + [services.tongyi]: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation", + [services.zhipu]: "https://open.bigmodel.cn/api/paas/v4/chat/completions", + [services.xiaoniu]: "https://api.niutrans.com/NiuTransServer/translationXML", + [services.claude]: "https://api.anthropic.com/v1/messages", +} + +export const method = {POST: "POST", GET: "GET",}; \ No newline at end of file diff --git a/entrypoints/utils/declare.d.ts b/entrypoints/utils/declare.d.ts new file mode 100644 index 0000000..d28470c --- /dev/null +++ b/entrypoints/utils/declare.d.ts @@ -0,0 +1,2 @@ +declare module 'entrypoints/utils/declare'; +declare module 'js-beautify'; diff --git a/entrypoints/utils/model.ts b/entrypoints/utils/model.ts new file mode 100644 index 0000000..14e30e2 --- /dev/null +++ b/entrypoints/utils/model.ts @@ -0,0 +1,68 @@ +import {defaultOption, services} from "./option"; + +interface IMapping { + [key: string]: string; +} + +// 内包,存储额外信息 +interface IExtra { + [key: string]: any +} + +export class Config { + on: boolean; // 是否开启 + from: string; + to: string; + hotkey: string; + service: string; + token: IMapping; + ak: string; + sk: string; + model: IMapping; + customModel: IMapping; // 自定义模型名称 + proxy: IMapping; // 代理地址 + native: string; // 本地服务地址 + extra: IExtra; // 额外信息(内包信息) + system_role: IMapping; + user_role: IMapping; + count: number; // 翻译次数 + + constructor() { + this.on = true; + this.from = defaultOption.from; + this.to = defaultOption.to; + this.hotkey = defaultOption.hotkey; + this.service = defaultOption.service; + this.token = {}; + this.ak = ''; + this.sk = ''; + this.model = {}; + this.customModel = {}; + this.proxy = {}; + this.native = defaultOption.native; + this.extra = {}; + this.system_role = { + [services.openai]: defaultOption.system_role, + [services.gemini]: defaultOption.system_role, + [services.yiyan]: defaultOption.system_role, + [services.tongyi]: defaultOption.system_role, + [services.zhipu]: defaultOption.system_role, + [services.moonshot]: defaultOption.system_role, + [services.ollama]: defaultOption.system_role, + [services.claude]: defaultOption.system_role, + [services.infini]:defaultOption.system_role, + }, + this.user_role = { + [services.openai]: defaultOption.user_role, + [services.gemini]: defaultOption.user_role, + [services.yiyan]: defaultOption.user_role, + [services.tongyi]: defaultOption.user_role, + [services.zhipu]: defaultOption.user_role, + [services.moonshot]: defaultOption.user_role, + [services.ollama]: defaultOption.user_role, + [services.claude]: defaultOption.user_role, + [services.infini]: defaultOption.user_role, + }, + this.count = 0; + } +} \ No newline at end of file diff --git a/entrypoints/utils/option.ts b/entrypoints/utils/option.ts new file mode 100644 index 0000000..3e3b3ed --- /dev/null +++ b/entrypoints/utils/option.ts @@ -0,0 +1,195 @@ +export const services = { + // 机器翻译 + microsoft: 'microsoft', + deepL: 'deepL', + google: 'google', + xiaoniu: 'xiaoniu', + // AI 翻译 + openai: 'openai', + gemini: 'gemini', + yiyan: 'yiyan', + tongyi: 'tongyi', + zhipu: 'zhipu', + moonshot: 'moonshot', + claude: 'claude', + ollama: 'ollama', + infini: 'infini', + // 阵营划分 + machine: new Set(["microsoft", "deepL", "google", "xiaoniu"]), + ai: new Set(["openai", "gemini", "yiyan", "tongyi", "zhipu", "moonshot", "claude", "ollama", "infini"]), + // 需要 token,或者 ak/sk + useToken: new Set(["openai", "gemini", "tongyi", "zhipu", "moonshot", "claude", "deepL", "xiaoniu", "infini"]), + useAkSk: new Set(["yiyan"]), + // 需要 model + useModel: new Set(["openai", "gemini", "yiyan", "tongyi", "zhipu", "moonshot", "claude", "ollama", "infini"]), + // 支持代理 + useProxy: new Set(["openai", "gemini", "claude", "google"]), + // 函数 + isMachine: (service: string) => services.machine.has(service), + isAI: (service: string) => services.ai.has(service), + isNative: (service: string) => service === "ollama", + isUseToken: (service: string) => services.useToken.has(service), + isUseAkSk: (service: string) => services.useAkSk.has(service), + isUseProxy: (service: string) => services.useProxy.has(service), + isUseModel: (service: string) => services.useModel.has(service), +} + +export const customModelString = "自定义模型" +export const models = new Map>([ + [services.openai, ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo-preview", customModelString]], + [services.gemini, ["gemini-pro", customModelString]], + [services.yiyan, ["ERNIE-Bot 4.0", "ERNIE-Bot"]], // 因文心一言模式不同,暂不支持自定义模型(还需根据model获取最终的url请求参数) + [services.tongyi, ["qwen-turbo", "qwen-plus", "qwen-max", "qwen-max-longcontext", customModelString]], + [services.zhipu, ["glm-4", "glm-4v", "glm-3-turbo", customModelString]], + [services.moonshot, ["moonshot-v1-8k", customModelString]], + [services.claude, ["claude3-Haiku", "claude3-Sonnet", "claude3-Opus"]], // claude 也不支持自定义模型 + [services.ollama, ["gemma:7b", "llama2:7b", "mistral:7b", customModelString]], + [services.infini, ["infini-megrez-7b","llama-2-13b-chat","qwen-14b-chat", "llama-2-70b-chat", "qwen-72b-chat",customModelString]], +]); + +export const options = { + on: [{ + value: true, + label: "开启" + }, { + value: false, + label: "关闭" + }], + form: [ + { + value: 'auto', + label: '自动检测', + }, + ], + to: [ + { + value: 'zh-Hans', + label: '中文', + }, + { + value: 'en', + label: '英语', + }, + { + value: 'ja', + label: '日语', + }, + { + value: 'ko', + label: '韩语', + }, + { + value: 'fr', + label: '法语', + }, + { + value: 'ru', + label: '俄语', + }, + ], + keys: [ + { + value: "Control", + label: "Ctrl" + }, + { + value: "Alt", + label: "Alt" + }, + { + value: "Shift", + label: "Shift" + }, + // 反引号键 + { + value: "`", + label: "波浪号键" + }, + ], + services: [ + { + value: 'machine', + label: '机器翻译', + disabled: true, + }, + { + value: services.microsoft, + label: '微软翻译', + }, + { + value: services.deepL, + label: 'DeepL翻译', + }, + { + value: services.xiaoniu, + label: '小牛翻译', + }, + // free 接口,翻译 html 会出现问题,隐藏。 + // { + // value: services.google, + // label: 'Google翻译', + // }, + { + value: 'ai', + label: 'AI翻译', + disabled: true, + }, + { + value: services.openai, + label: 'OpenAI', + model: "gpt-3.5-turbo", + }, + { + value: services.claude, + label: 'Claude', + model: "Haiku", + }, + { + value: services.gemini, + label: 'Gemini', + model: "gemini-pro", + }, + { + value: services.moonshot, + label: 'Moonshot', + model: "moonshot-v1-8k", + }, + { + value: services.zhipu, + label: '智谱清言', + model: "glm-3-turbo", + }, + { + value: services.tongyi, + label: '通义千问', + model: "qwen-turbo", + }, + { + value: services.yiyan, + label: '文心一言', + model: "ERNIE-Bot", + }, + { + value: services.infini, + label: '无向芯穹Infini', + }, + { + value: services.ollama, + label: 'Ollama本地模型', + }, + ], +} + +export const defaultOption = { + on: true, + from: 'auto', + to: 'zh-Hans', + hotkey: 'Control', + service: services.microsoft, + native: 'http://localhost:11434/v1/chat/completions', + system_role: 'You are a professional, authentic translation engine, only returns translations.', + user_role: `Please translate them into {{to}}, please do not explain my original text.: + +{{origin}}`, + count: 0, +} \ No newline at end of file diff --git a/entrypoints/utils/template.ts b/entrypoints/utils/template.ts new file mode 100644 index 0000000..ed9615b --- /dev/null +++ b/entrypoints/utils/template.ts @@ -0,0 +1,134 @@ +// 消息模板工具 +import {Config} from "./model"; +import {customModelString, defaultOption, services} from "./option"; + +// openai 格式的消息模板(通用) +export function openaiMsgTemplate(config: Config, origin: string) { + // 检测是否使用自定义模型 + let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] + + let system = config.system_role[config.service] || defaultOption.system_role; + let user = ((config.user_role[config.service] || defaultOption.user_role)).replace('{{to}}', config.to).replace('{{origin}}', 'hello'); + + return JSON.stringify({ + 'model': model, + "temperature": 0.3, + 'messages': [ + {'role': 'system', 'content': system}, + { + 'role': 'user', + 'content': user + }, + {'role': "assistant", 'content': '你好'}, + {'role': 'user', 'content': origin} + ] + }) +} + +// gemini +export function geminiMsgTemplate(config: Config, origin: string) { + let user = (config.user_role[config.service] || defaultOption.user_role).replace('{{to}}', config.to).replace('{{origin}}', 'hello'); + + return JSON.stringify({ + "contents": [ + { + "role": "user", + "parts": [{"text": user}] + }, + { + "role": "model", + "parts": [{"text": "你好"}] + }, + { + "role": "user", + "parts": [{"text": origin}] + }] + }) +} + +// claude +export function claudeMsgTemplate(config: Config, origin: string) { + let model = config.model[services.claude]; + if (model === "Haiku") model = "claude-3-haiku-20240307"; + else if (model === "Sonnet") model = "claude-3-sonnet-20240229"; + else if (model === "Opus") model = "claude-3-opus-20240229"; + + let system = config.system_role[config.service] || defaultOption.system_role; + let user = (config.user_role[config.service] || defaultOption.user_role).replace('{{to}}', config.to).replace('{{origin}}', 'hello'); + + return JSON.stringify({ + model: model, + max_tokens: 4096, + stream: false, + system: system, + messages: [ + {role: "user", content: user}, + {role: "assistant", content: "你好"}, + {role: "user", content: origin} + ] + }) +} + +// 通义千问 +export function tongyiMsgTemplate(config: Config, origin: string) { + let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] + + let system = config.system_role[config.service] || defaultOption.system_role; + let user = (config.user_role[config.service] || defaultOption.user_role).replace('{{to}}', config.to).replace('{{origin}}', 'hello'); + + return JSON.stringify({ + "model": model, + "input": { + "messages": [ + {"role": "system", "content": system}, + { + "role": "user", + "content": user + }, + {"role": "assistant", "content": "你好"}, + {"role": "user", "content": origin} + ] + }, + "parameters": {} + }) +} + +// 文心一言 +export function yiyanMsgTemplate(config: Config, origin: string) { + let user = (config.user_role[config.service] || defaultOption.user_role).replace('{{to}}', config.to).replace('{{origin}}', 'hello'); + + return JSON.stringify({ + 'temperature': 0.3, // 随机度 + 'disable_search': true, // 禁用搜索 + 'messages': [ + { + "role": "user", + "content": user + }, + {"role": "assistant", "content": "你好"}, + {"role": "user", "content": origin} + ], + }) +} + + +// ollama +export function ollamaMsgTemplate(config: Config, origin: string) { + let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] + + let system = config.system_role[config.service] || defaultOption.system_role; + let user = (config.user_role[config.service] || defaultOption.user_role).replace('{{to}}', config.to).replace('{{origin}}', origin); + + return JSON.stringify({ + 'model': model, + "stream": false, + "temperature": 0.1, + 'messages': [ + {'role': 'system', 'content': system}, + { + 'role': 'user', + 'content': user, + }, + ] + }) +} \ No newline at end of file diff --git a/entrypoints/utils/tip.ts b/entrypoints/utils/tip.ts new file mode 100644 index 0000000..6d9eb66 --- /dev/null +++ b/entrypoints/utils/tip.ts @@ -0,0 +1,27 @@ +import {ElMessage} from "element-plus"; + +const prefix = "流畅阅读:"; + +function _sendErrorMessage(message: string) { + ElMessage({message: prefix + message, type: 'error'}); +} + +function _sendSuccessMessage(message: string) { + ElMessage({message: prefix + message, type: 'success'}); +} + +// 修改防抖限流函数(允许传递参数) +export function throttle(fn: Function, interval: number) { + let last = 0; // 维护上次执行的时间 + return function (...args: any[]) { // 使用 rest 参数来传递所有参数 + const now = Date.now(); + if (now - last >= interval) { + last = now; + fn.apply(this, args); // 使用 apply 来传递参数数组 + } + }; +} + +// 使用防抖函数包装,1s 内只能发送一次消息 +export const sendErrorMessage = throttle(_sendErrorMessage, 1000); +export const sendSuccessMessage = throttle(_sendSuccessMessage, 1000); diff --git a/go.mod b/go.mod deleted file mode 100644 index 192c450..0000000 --- a/go.mod +++ /dev/null @@ -1,73 +0,0 @@ -module FluentRead - -go 1.21.3 - -require ( - github.com/alibabacloud-go/alimt-20181012/v2 v2.2.0 - github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 - github.com/alibabacloud-go/tea v1.2.1 - github.com/alibabacloud-go/tea-utils/v2 v2.0.4 - github.com/anhao/go-ernie v1.0.9 - github.com/bas24/googletranslatefree v0.0.0-20231117033553-f5859fe54d30 - github.com/gin-contrib/cors v1.5.0 - github.com/gin-gonic/gin v1.9.1 - github.com/go-redis/redis/v8 v8.11.5 - github.com/go-sql-driver/mysql v1.7.1 - github.com/hwfy/translate v0.0.0-20180905054239-cd3ba460f435 - github.com/sashabaranov/go-openai v1.17.11 - github.com/stretchr/testify v1.8.4 - go.uber.org/zap v1.26.0 - golang.org/x/net v0.19.0 - gorm.io/driver/mysql v1.5.2 - gorm.io/gorm v1.25.5 -) - -require ( - github.com/Bistutu/whatlanggo v0.0.0-20240302125929-e41be972c447 // indirect - github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect - github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect - github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect - github.com/alibabacloud-go/openapi-util v0.1.0 // indirect - github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect - github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect - github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect - github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect - github.com/alibabacloud-go/tea-utils v1.3.6 // indirect - github.com/alibabacloud-go/tea-xml v1.1.3 // indirect - github.com/aliyun/credentials-go v1.3.1 // indirect - github.com/bytedance/sonic v1.10.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.0 // indirect - github.com/clbanning/mxj/v2 v2.5.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.15.5 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tjfoc/gmsm v1.3.2 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.5.0 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect - google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/ini.v1 v1.56.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 5ae29d1..0000000 --- a/go.sum +++ /dev/null @@ -1,279 +0,0 @@ -github.com/Bistutu/whatlanggo v0.0.0-20240302125929-e41be972c447 h1:Fdef/lAx+JkllwU/XWjf6t1mjELO3T9/UlAuC3miJ6w= -github.com/Bistutu/whatlanggo v0.0.0-20240302125929-e41be972c447/go.mod h1:e73xVVRNHYraHwNaxfiFIzd/6TjcEhyPDPhuRiR4YnM= -github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= -github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= -github.com/alibabacloud-go/alimt-20181012/v2 v2.2.0 h1:9AwDOjOZvhycl60jlXuMBSSl52rpWlcAuwxJOAQM4Bo= -github.com/alibabacloud-go/alimt-20181012/v2 v2.2.0/go.mod h1:4gZhZ+BvRg/k14Z8SZnmu86zNqjslSpcC1wFl0jabl4= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 h1:yyolbgHfV2Tp91vMjO/CF5aOxKG+UgdVAeUoloEQI3E= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg= -github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= -github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= -github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= -github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= -github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= -github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= -github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= -github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 h1:L0TIjr9Qh/SLVc1yPhFkcB9+9SbCNK/jPq4ZKB5zmnc= -github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1/go.mod h1:EKxBRDLcMzwl4VLF/1WJwlByZZECJawPXUvinKMsTTs= -github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= -github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.10/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.12/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= -github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= -github.com/alibabacloud-go/tea v1.2.1 h1:rFF1LnrAdhaiPmKwH5xwYOKlMh66CqRwPUTzIK74ask= -github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= -github.com/alibabacloud-go/tea-fileform v1.1.1 h1:1YG6erAP3joQ0XdCXYIotuD7zyOM6qCR49xkp5FZDeU= -github.com/alibabacloud-go/tea-fileform v1.1.1/go.mod h1:ZeCV91o4ISmxidd686f0ebdS5EDHWU+vW+TkjLhrsFE= -github.com/alibabacloud-go/tea-oss-sdk v1.1.3 h1:EhAHI6edMeqgkZEqP7r4nc9iMWAUBKGxJHoBsOSKTtU= -github.com/alibabacloud-go/tea-oss-sdk v1.1.3/go.mod h1:yUnodpR3Bf2rudLE7V/Gft5txjJF30Pk+hH77K/Eab0= -github.com/alibabacloud-go/tea-oss-utils v1.1.0 h1:y65crjjcZ2Pbb6UZtC2deuIZHDVTS3IaDWE7M9nVLRc= -github.com/alibabacloud-go/tea-oss-utils v1.1.0/go.mod h1:PFCF12e9yEKyBUIn7X1IrF/pNjvxgkHy0CgxX4+xRuY= -github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= -github.com/alibabacloud-go/tea-utils v1.3.6 h1:bVjrxHztM8hAs6nOfLWCgxQfAtKb9RgFFMV6J3rdvB4= -github.com/alibabacloud-go/tea-utils v1.3.6/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= -github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4= -github.com/alibabacloud-go/tea-utils/v2 v2.0.4 h1:SoFgjJuO7pze88j9RBJNbKb7AgTS52O+J5ITxc00lCs= -github.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= -github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= -github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= -github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= -github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= -github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= -github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28= -github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= -github.com/anhao/go-ernie v1.0.9 h1:j2+vsZcgOcwFGiy+hY2rqm/Akw4R32ftmap/uCCEkQY= -github.com/anhao/go-ernie v1.0.9/go.mod h1:lNCznvV3M7RIQqzouq7UFC1sL8UPmCzIKDWvKV6ecQE= -github.com/bas24/googletranslatefree v0.0.0-20231117033553-f5859fe54d30 h1:dvq7NKKclmPTAaB4iPRo5L4EBSxCIlVI1nxCRqX8fVA= -github.com/bas24/googletranslatefree v0.0.0-20231117033553-f5859fe54d30/go.mod h1:ntTdGCe6WzFmHjox8vK2FZ2KLyh0IFxw43B6XCg0zf4= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= -github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= -github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= -github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= -github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/hwfy/translate v0.0.0-20180905054239-cd3ba460f435 h1:73W9eZK1sJr9XwmQAdLQOo51jEsRftJKsyKAQjLLRmE= -github.com/hwfy/translate v0.0.0-20180905054239-cd3ba460f435/go.mod h1:+SmwClOBLl633X5SetQJ0AdkXFyAMDVeXmyX1KrX59M= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/sashabaranov/go-openai v1.17.11 h1:XVr00J8JymJVx8Hjbh/5mG0V4PQHRarBU3v7k2x6MR0= -github.com/sashabaranov/go-openai v1.17.11/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= -github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= -github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= -golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y= -gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= -gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= -gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/handler/detect.go b/handler/detect.go deleted file mode 100644 index 27759dd..0000000 --- a/handler/detect.go +++ /dev/null @@ -1,39 +0,0 @@ -package handler - -import ( - "net/url" - - "github.com/gin-gonic/gin" - - "FluentRead/logic" - "FluentRead/misc/log" -) - -var detectCount = 1 - -func DetectHandler(c *gin.Context) { - log.Infof("第 %d 次 detect 请求", detectCount) - detectCount++ - - // 获取请求参数,post 请求 text=正文 - rawData, _ := c.GetRawData() - rawString := string(rawData) - - // 检测 - if len(rawString) >= 5 && rawString[:5] == "text=" { - rawString = rawString[5:] - } else { - c.JSON(400, gin.H{"error": "请求格式错误"}) - return - } - - // 进行URL解码 - decodedString, err := url.QueryUnescape(rawString) - if err != nil { - c.JSON(400, gin.H{"error": "解码错误"}) - return - } - - language := logic.DetectLanguage(decodedString) - c.JSON(200, gin.H{"language": language}) -} diff --git a/handler/preread.go b/handler/preread.go deleted file mode 100644 index 497a3db..0000000 --- a/handler/preread.go +++ /dev/null @@ -1,26 +0,0 @@ -package handler - -import ( - "github.com/gin-gonic/gin" - - "FluentRead/logic" - "FluentRead/misc/log" - "FluentRead/models" -) - -var count = 1 - -func PreReadHandler(c *gin.Context) { - - log.Infof("第 %d 次 preread 请求", count) - count++ - - data, err := logic.PreRead(c) - if err != nil { - c.JSON(500, err) - return - } - - c.JSON(200, models.Success(data)) - return -} diff --git a/handler/read.go b/handler/read.go deleted file mode 100644 index bfff238..0000000 --- a/handler/read.go +++ /dev/null @@ -1,48 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/url" - - "github.com/gin-gonic/gin" - - "FluentRead/logic" - "FluentRead/models" - "FluentRead/utils" -) - -func ReadHandler(c *gin.Context) { - - // 获取请求参数与验证格式 - rawData, _ := c.GetRawData() - var readRequest *models.ReadRequest - if err := json.Unmarshal(rawData, &readRequest); err != nil { - utils.CallbackBadRequest(c, err) - return - } - - // 检查 url 是否合法 - if parse, err := url.Parse(readRequest.Page); err != nil || parse.Host == "" { - utils.CallbackBadRequest(c, err) - return - } - - switch { - case len(readRequest.HashList) > 0: - // 批量读取 - read, err := logic.BatchRead(c, readRequest.HashList) - if err != nil { - utils.CallbackSystemError(c, err) - return - } - c.JSON(200, models.Success(read)) - default: - // 按 host 全文读取 - resp, err := logic.PageRead(c, readRequest.Page) - if err != nil { - utils.CallbackSystemError(c, err) - return - } - c.JSON(200, models.Success(resp)) - } -} diff --git a/handler/router.go b/handler/router.go deleted file mode 100644 index 3cae543..0000000 --- a/handler/router.go +++ /dev/null @@ -1,23 +0,0 @@ -package handler - -import ( - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -func NewRouter() *gin.Engine { - - engine := gin.Default() - - // 允许所有跨域请求 - engine.Use(cors.Default()) - // 加载静态资源 - engine.StaticFile("/", "./static") - - // 绑定路由 - engine.POST("/preread", PreReadHandler) // 预读接口 - engine.POST("/read", ReadHandler) // 按 host 全文读取接口 - engine.POST("/detect", DetectHandler) // 语言检测接口 - - return engine -} diff --git a/logic/detect.go b/logic/detect.go deleted file mode 100644 index 2644173..0000000 --- a/logic/detect.go +++ /dev/null @@ -1,8 +0,0 @@ -package logic - -import "github.com/Bistutu/whatlanggo" - -func DetectLanguage(origin string) string { - info := whatlanggo.Detect(origin) - return whatlanggo.Scripts[info.Script] -} diff --git a/logic/detect_test.go b/logic/detect_test.go deleted file mode 100644 index b7e636e..0000000 --- a/logic/detect_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package logic - -import ( - "fmt" - "testing" -) - -func BenchmarkDetect(b *testing.B) { - for i := 0; i < b.N; i++ { - DetectLanguage("text=正文") - } -} - -func TestDetectLanguage(t *testing.T) { - fmt.Println(DetectLanguage("哈哈哈")) -} diff --git a/logic/preread.go b/logic/preread.go deleted file mode 100644 index 4ef9cbf..0000000 --- a/logic/preread.go +++ /dev/null @@ -1,50 +0,0 @@ -package logic - -import ( - "context" - "encoding/json" - - "FluentRead/misc/log" - "FluentRead/models" - "FluentRead/repo/cache" - "FluentRead/repo/db" -) - -const ( - prereadKey = "preread" -) - -func PreRead(ctx context.Context) (map[string]string, error) { - - // 1、读缓存 - data, err := cache.GetKey(ctx, prereadKey) - if err != nil { - log.Warnf("获取缓存失败: %v", err) - } - if data != "" { - var pageMap map[string]string - err = json.Unmarshal([]byte(data), &pageMap) - if err != nil { - log.Errorf("解析缓存失败: %v", err) - return nil, err - } - return pageMap, nil - } - - // 2、读数据库 - pages, err := db.ListPages(ctx) - if err != nil { - log.Errorf("获取所有页面信息失败: %v", err) - return nil, err - } - pageMap := models.PageToMap(pages) - - // 3、写缓存 - bytes, _ := json.Marshal(pageMap) - err = cache.SetKey(ctx, prereadKey, bytes) - if err != nil { - log.Warnf("写入缓存失败: %v", err) - } - - return pageMap, nil -} diff --git a/logic/preread_test.go b/logic/preread_test.go deleted file mode 100644 index b860cfd..0000000 --- a/logic/preread_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package logic - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPreRead(t *testing.T) { - ctx := context.Background() - preRead, err := PreRead(ctx) - assert.NoError(t, err) - t.Log(preRead) -} diff --git a/logic/read.go b/logic/read.go deleted file mode 100644 index a0d6a64..0000000 --- a/logic/read.go +++ /dev/null @@ -1,74 +0,0 @@ -package logic - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/gin-gonic/gin" - - "FluentRead/misc/log" - "FluentRead/models" - "FluentRead/repo/cache" - "FluentRead/repo/db" - "FluentRead/utils" -) - -const ( - pageCacheConst = "page:%s" // page:link - shortTimeOut = 30 * time.Second - longTimeOut = 24 * time.Hour -) - -// PageRead 按域名读取 -func PageRead(ctx *gin.Context, link string) (transMap map[string]string, err error) { - - link = utils.GetHost(link) - - // 1、查缓存,获取失败不影响后续流程 - key := fmt.Sprintf(pageCacheConst, link) - transString, err := cache.GetKey(ctx, key) - if err != nil { - log.Warnf("缓存读取失败:%v", err) - } - if len(transString) > 0 { - err = json.Unmarshal([]byte(transString), &transMap) - if err != nil { - log.Errorf("缓存数据转换失败:%v", err) - return nil, err - } - log.Infof("缓存命中:%s", link) - return transMap, nil - } - - // 2、查数据库 - transs, err := db.GetAllByPage(ctx, link) - if err != nil { - log.Errorf("全文读取失败:%v", err) - return nil, err - } - transMap = models.BatchTransToMap(transs) - - // 3、写缓存 and 防缓存穿透 - timeout := shortTimeOut - if len(transs) > 0 { - timeout = longTimeOut - } - err = cache.SetKeyWithTimeout(ctx, key, timeout, models.MapToBytes(transMap)) - if err != nil { - log.Warnf("缓存写入失败:%v", err) - } - - return transMap, nil -} - -// BatchRead 批量读取 -func BatchRead(ctx *gin.Context, hashs []string) (map[string]string, error) { - - transModels, err := db.BatchGet(ctx, hashs) - if err != nil { - log.Errorf("批量读取失败:%v", err) - return nil, err - } - return models.BatchTransToMap(transModels), nil -} diff --git a/logic/read_test.go b/logic/read_test.go deleted file mode 100644 index 4477ca0..0000000 --- a/logic/read_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package logic - -import ( - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -var ( - ctx = &gin.Context{} -) - -func TestReadByPage(t *testing.T) { - read, err := PageRead(ctx, "https://chat.openai.com/") - assert.NoError(t, err) - t.Log(read) -} diff --git a/logic/trans.go b/logic/trans.go deleted file mode 100644 index 6923eb8..0000000 --- a/logic/trans.go +++ /dev/null @@ -1,8 +0,0 @@ -package logic - -import "github.com/gin-gonic/gin" - -// TransHandler 翻译接口,接收16进制数据,返回翻译后的文本 -func TransHandler(c *gin.Context) { - // todo 待实现 -} diff --git a/main.go b/main.go deleted file mode 100644 index d96571f..0000000 --- a/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - - "FluentRead/handler" - "FluentRead/misc/log" - _ "FluentRead/repo/db" - "FluentRead/utils" -) - -func main() { - engine := handler.NewRouter() - // 如果环境变量端口为空,则使用默认端口 12345 - if err := engine.Run(fmt.Sprintf(":%s", utils.GetEnvDefault("PORT", "12345"))); err != nil { - log.Errorf("服务启动失败: %v", err) - panic(err) - } -} diff --git a/misc/images/approve.jpg b/misc/approve.jpg similarity index 100% rename from misc/images/approve.jpg rename to misc/approve.jpg diff --git a/misc/images/icon.png b/misc/images/icon.png deleted file mode 100644 index 39ad18d..0000000 Binary files a/misc/images/icon.png and /dev/null differ diff --git a/misc/images/install-1.png b/misc/images/install-1.png deleted file mode 100644 index 0529157..0000000 Binary files a/misc/images/install-1.png and /dev/null differ diff --git a/misc/images/install-2.png b/misc/images/install-2.png deleted file mode 100644 index 07a131d..0000000 Binary files a/misc/images/install-2.png and /dev/null differ diff --git a/misc/images/sample-1.png b/misc/images/sample-1.png deleted file mode 100644 index 0cdb008..0000000 Binary files a/misc/images/sample-1.png and /dev/null differ diff --git a/misc/images/sample-2.png b/misc/images/sample-2.png deleted file mode 100644 index 23ed381..0000000 Binary files a/misc/images/sample-2.png and /dev/null differ diff --git a/misc/images/sample-5.png b/misc/images/sample-5.png deleted file mode 100644 index 35b9052..0000000 Binary files a/misc/images/sample-5.png and /dev/null differ diff --git a/misc/images/sample-git-3.gif b/misc/images/sample-git-3.gif deleted file mode 100644 index 86ade01..0000000 Binary files a/misc/images/sample-git-3.gif and /dev/null differ diff --git a/misc/inspection/inspectDB_test.go b/misc/inspection/inspectDB_test.go deleted file mode 100644 index 7e713c6..0000000 --- a/misc/inspection/inspectDB_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package inspection - -import ( - "context" - "fmt" - "regexp" - "testing" - - "github.com/stretchr/testify/assert" - - "FluentRead/repo/db" - "FluentRead/utils" -) - -var ctx = context.Background() - -func TestAllEnglish(t *testing.T) { - // 如果数据库中存在 target 为纯[英文+数字]的翻译,则删除 - transs, err := db.ListTrans(ctx) - assert.NoError(t, err) - - for _, v := range transs { - if utils.IsAllEnglishAndNum(v.Target) { - t.Logf("纯[英文+数字],删除:%+v", v) - err := db.RemoveTrans(ctx, v) - assert.NoError(t, err) - } - } -} - -// 去除所有翻译后的前后中文双引号 -func TestQuotation(t *testing.T) { - ctx := context.Background() - transs, err := db.ListTrans(ctx) - assert.NoError(t, err) - - myRegex, _ := regexp.Compile(`^“(.*)”$`) - for _, v := range transs { - // 正则表达式提取中间的内容 - matches := myRegex.FindStringSubmatch(v.Target) - if len(matches) > 1 { - fmt.Println("去除前后的中文双引号:", v.Target) - v.Target = matches[1] - err := db.UpdateTrans(ctx, v) - assert.NoError(t, err) - } - } -} diff --git a/misc/log/log.go b/misc/log/log.go deleted file mode 100644 index 845a59d..0000000 --- a/misc/log/log.go +++ /dev/null @@ -1,72 +0,0 @@ -package log - -import ( - "go.uber.org/zap" -) - -var logger *zap.SugaredLogger - -func init() { - baseLogger, _ := zap.NewDevelopment() - logger = baseLogger.Sugar() -} - -// Info logs a non-formatted info message. -func Info(args ...interface{}) { - logger.Info(args...) -} - -// Infof logs an info message with formatting. -func Infof(template string, args ...interface{}) { - logger.Infof(template, args...) -} - -// Infow logs an info message with named fields. -func Infow(msg string, keysAndValues ...interface{}) { - logger.Infow(msg, keysAndValues...) -} - -// Debug logs a non-formatted debug message. -func Debug(args ...interface{}) { - logger.Debug(args...) -} - -// Debugf logs a debug message with formatting. -func Debugf(template string, args ...interface{}) { - logger.Debugf(template, args...) -} - -// Debugw logs a debug message with named fields. -func Debugw(msg string, keysAndValues ...interface{}) { - logger.Debugw(msg, keysAndValues...) -} - -// Warn logs a non-formatted warning message. -func Warn(args ...interface{}) { - logger.Warn(args...) -} - -// Warnf logs a warning message with formatting. -func Warnf(template string, args ...interface{}) { - logger.Warnf(template, args...) -} - -// Warnw logs a warning message with named fields. -func Warnw(msg string, keysAndValues ...interface{}) { - logger.Warnw(msg, keysAndValues...) -} - -// Error logs a non-formatted error message. -func Error(args ...interface{}) { - logger.Error(args...) -} - -// Errorf logs an error message with formatting. -func Errorf(template string, args ...interface{}) { - logger.Errorf(template, args...) -} - -// Errorw logs an error message with named fields. -func Errorw(msg string, keysAndValues ...interface{}) { - logger.Errorw(msg, keysAndValues...) -} diff --git a/misc/manuals/manual_json b/misc/manuals/manual_json deleted file mode 100644 index e69de29..0000000 diff --git a/misc/manuals/manual_test.go b/misc/manuals/manual_test.go deleted file mode 100644 index a23ebf7..0000000 --- a/misc/manuals/manual_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package manuals - -import ( - "bufio" - "context" - "encoding/json" - "fmt" - "io" - "log" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - - "FluentRead/models" - "FluentRead/repo/cache" - "FluentRead/repo/db" - "FluentRead/utils" -) - -var ( - ctx = context.Background() - link = utils.GetHost( - //"https://openai.com/", - //"https://platform.openai.com/apps", - "https://chat.openai.com/", - //"https://hub.docker.com/search?q=", - //"https://mvnrepository.com/artifact/com.alibaba/fastjson/2.0.44", - //"https://help.openai.com/en/articles/8554397-creating-a-gpt", - //"https://platform.openai.com/docs/overview", - //"https://mvnrepository.com/repos", - //"https://www.nexusmods.com/", - //"https://users.nexusmods.com/", - //"https://www.coze.com/space/7313028917407842311/plugin", - ) -) - -func TestInsertTrans_JSON(t *testing.T) { - pageId, err := db.InsertOrUpdatePage(ctx, &models.Page{Link: link}) - assert.NoError(t, err) - // 删除缓存 - cache.RemoveKey(ctx, "preread") - cache.RemoveKey(ctx, fmt.Sprintf("page:%s", link)) - - // 打开文件 - file, err := os.Open("manual.json") - assert.NoError(t, err) - defer file.Close() - bytes, _ := io.ReadAll(file) - - var data []string - err = json.Unmarshal(bytes, &data) - assert.NoError(t, err) - - for _, line := range data { - line = strings.TrimSpace(line) - if !utils.IsEnglish(line) { - continue - } - - signature := models.Signature(link + line) - db.InsertTrans(ctx, &models.Translation{ - Source: line, - Target: "", - Hash: signature, - Translated: false, - TargetType: 0, - PageId: pageId, - }) - } -} - -func TestInsertTrans(t *testing.T) { - pageId, err := db.InsertOrUpdatePage(ctx, &models.Page{Link: link}) - assert.NoError(t, err) - // 删除缓存 - cache.RemoveKey(ctx, "preread") - cache.RemoveKey(ctx, fmt.Sprintf("page:%s", link)) - // 打开文件 - file, err := os.Open("manual") - assert.NoError(t, err) - defer file.Close() - - // 创建 Scanner 循环读取文件的每一行 - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) // Text()返回当前行的内容 - if !utils.IsEnglish(line) { - continue - } - - signature := models.Signature(link + line) - db.InsertTrans(ctx, &models.Translation{Source: line, Target: "", Hash: signature, Translated: false, TargetType: 0, PageId: pageId}) - } - // 检查Scan过程中是否有错误发生(文件结尾除外) - if err := scanner.Err(); err != nil { - log.Fatal(err) - } -} diff --git a/misc/images/sample-git-1.gif b/misc/sample-git-1.gif similarity index 100% rename from misc/images/sample-git-1.gif rename to misc/sample-git-1.gif diff --git a/misc/images/sample-git-2.gif b/misc/sample-git-2.gif similarity index 100% rename from misc/images/sample-git-2.gif rename to misc/sample-git-2.gif diff --git a/misc/images/sample-git-4.gif b/misc/sample-git-3.gif similarity index 100% rename from misc/images/sample-git-4.gif rename to misc/sample-git-3.gif diff --git a/misc/screenshot-1.png b/misc/screenshot-1.png new file mode 100644 index 0000000..63f1546 Binary files /dev/null and b/misc/screenshot-1.png differ diff --git a/misc/screenshot-2.png b/misc/screenshot-2.png new file mode 100644 index 0000000..aa589b4 Binary files /dev/null and b/misc/screenshot-2.png differ diff --git a/misc/spider/spider.go b/misc/spider/spider.go deleted file mode 100644 index 2aa7419..0000000 --- a/misc/spider/spider.go +++ /dev/null @@ -1,184 +0,0 @@ -package spider - -import ( - "context" - "fmt" - "io" - "net/url" - "strings" - - "golang.org/x/net/html" - - "FluentRead/misc/log" - "FluentRead/models" - "FluentRead/repo/db" - "FluentRead/utils" -) - -const ( - doubleSlash = "://" -) - -// 爬取单个页面 -func snifferSingle(ctx context.Context, link string) error { - - count := 1 // 统计文字数量 - - root, err := fetchHTML(link) - if err != nil { - log.Errorf("链接访问失败: %v", err) - return err - } - - parsedUrl, err := url.Parse(link) - if err != nil { - log.Errorf("链接解析失败: %v", err) - return err - } - // 只取 host - link = parsedUrl.Host - - pageId, err := db.InsertPage(ctx, &models.Page{Link: link}) - if err != nil { - log.Errorf("数据库 pages 插入链接失败: %v", err) - return err - } - fmt.Printf("pageId: %d, link: %s\n", pageId, link) - - // dfs 遍历 - parseDFS(ctx, parsedUrl, root, nil, pageId, count) - - return nil -} - -// sniffer 按友链爬取网页文本 -func sniffer(ctx context.Context, link string) error { - // 计数器 - count := 1 - // 页面链接队列 - queue := models.Queue{} - - // bfs 遍历 - queue.PushBack(link) - visited := make(map[string]bool) - - for queue.Len() > 0 { - // 弹出一个链接 - link := queue.Pop() - - // 解析初始链接,获取主机名 - parsedUrl, err := url.Parse(link) - if err != nil { - log.Errorf("链接解析失败: %v", err) - continue - } - link = parsedUrl.Host - - // 如果已经访问过,则跳过 - if visited[link] { - continue - } - visited[link] = true - // pages 表插入新链接 - pageId, err := db.InsertPage(ctx, &models.Page{Link: link}) - if err != nil { - log.Errorf("数据库 pages 插入链接失败: %v", err) - return err - } - fmt.Printf("pageId: %d, link: %s\n", pageId, link) - - root, err := fetchHTML(parsedUrl.Scheme + doubleSlash + link + parsedUrl.Path) - if err != nil { - continue - } - - // dfs 遍历 - parseDFS(ctx, parsedUrl, root, &queue, pageId, count) - } - return nil -} - -// 获取 html 资源 -func fetchHTML(link string) (*html.Node, error) { - resp, err := utils.Get(link) - if err != nil { - log.Errorf("链接访问失败: %v", err) - return nil, err - } - defer resp.Body.Close() - - bytes, _ := io.ReadAll(resp.Body) - fmt.Println(string(bytes)) - - // 解析页面后,主动关闭资源 - root, err := html.Parse(resp.Body) - if err != nil { - log.Errorf("html 页面解析失败: %v", err) - return nil, err - } - return root, nil -} - -// dfs 遍历 -func parseDFS(ctx context.Context, parsedUrl *url.URL, node *html.Node, queue *models.Queue, pageId uint, count int) int { - // node.Type 为节点类型,node.Data 为标签名 - switch { - case node.Type == html.ElementNode && utils.IsContain(node.Data, []string{"script", "style", "img", "noscript"}): - return count - case node.Type == html.ElementNode && node.Data == "a": - parseHref(ctx, parsedUrl, node, queue) - fallthrough - case node.Type == html.TextNode: - parseText(ctx, parsedUrl, node, pageId, count) - count++ - } - for c := node.FirstChild; c != nil; c = c.NextSibling { - count = parseDFS(ctx, parsedUrl, c, queue, pageId, count) - } - return count -} - -// 解析文本 -func parseText(ctx context.Context, parsedUrl *url.URL, node *html.Node, pageId uint, count int) { - // 获取标准化的域名:host + path - - text := strings.TrimSpace(node.Data) - text = strings.ReplaceAll(text, "\u00A0", " ") - - // 如果文本长度大于 0,且全是英文,则插入数据库 - if len(text) > 0 && utils.IsNonChinese(text) { - // 签名 - signature := models.Signature(parsedUrl.Host + text) - - db.InsertTrans(ctx, &models.Translation{ - Source: text, - Target: "", - Hash: signature, - Translated: false, - TargetType: 0, - PageId: pageId, - }) // link + text,去重 Source: text, TargetType: 1, Target: "", - - fmt.Println(count, parsedUrl, signature, text) - } -} - -// 解析链接 -func parseHref(ctx context.Context, parsedUrl *url.URL, n *html.Node, queue *models.Queue) { - host := parsedUrl.Host - for _, a := range n.Attr { - if a.Key == "href" { - href, err := url.Parse(a.Val) - if err != nil { - continue - } - // 如果主机名匹配,将链接加入队列 - if href.Host == host { - queue.PushBack(a.Val) - fmt.Println("新链接: ", a.Val) - } - } - } -} - -// 按行读取文件,返回字符串切片 diff --git a/misc/spider/spiderTxt.txt b/misc/spider/spiderTxt.txt deleted file mode 100644 index a6691f8..0000000 --- a/misc/spider/spiderTxt.txt +++ /dev/null @@ -1,3 +0,0 @@ -https://www.consul.io/ -https://orbstack.dev/ -https://www.docker.com/ diff --git a/misc/spider/spider_test.go b/misc/spider/spider_test.go deleted file mode 100644 index b3c5e1f..0000000 --- a/misc/spider/spider_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package spider - -import ( - "context" - "testing" - - "FluentRead/utils" -) - -var ( - ctx = context.Background() -) - -func TestSnifferSingle(t *testing.T) { - //snifferSingle(ctx, "https://github.com/") - //snifferSingle(ctx, "https://www.consul.io/") - //snifferSingle(ctx, "https://orbstack.dev/") - //snifferSingle(ctx, "https://orbstack.dev/dashboard") - //snifferSingle(ctx, "https://orbstack.dev/pricing") - snifferSingle(ctx, "https://mvnrepository.com/artifact/com.alibaba/fastjson") - //snifferSingle(ctx, "https://docs.orbstack.dev/legal/terms") -} - -// 递归爬取网页及其子页面 -func TestSpider(t *testing.T) { - //sniffer(ctx, "https://github.com/") - //sniffer(ctx, "https://www.docker.com/") - //sniffer(ctx, "https://www.docker.com/company/") - //sniffer(ctx, "https://www.consul.io/") - //sniffer(ctx, "https://www.coze.com/") - sniffer(ctx, "https://orbstack.dev/") -} - -func TestDiscoverLetter(t *testing.T) { - t.Log(utils.IsNonChinese("hello")) - t.Log(utils.IsNonChinese("hello,好")) - t.Log(utils.IsNonChinese("hello,😄")) -} - -func BenchmarkDiscoverLetter(b *testing.B) { - for i := 0; i < b.N; i++ { - utils.IsNonChinese("hello") - } -} diff --git a/misc/test/hash_test.go b/misc/test/hash_test.go deleted file mode 100644 index 4e5898f..0000000 --- a/misc/test/hash_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "fmt" - "testing" -) - -func TestHashText(t *testing.T) { - data := []byte("some data to hash") - fmt.Printf("%x\n", md5.Sum(data)) - fmt.Printf("%x\n", sha1.Sum(data)) - fmt.Printf("%x\n", sha256.Sum256(data)) - fmt.Printf("%x\n", sha512.Sum512(data)) -} - -func TestBatchParse(t *testing.T) { - data := []byte("Get OrbStack") - sum := sha1.Sum(data) - fmt.Printf("%x\n", sum[10:]) -} - -func TestLength(t *testing.T) { - data := []byte("some data to hash") - - // 只取一半 - - sum := md5.Sum(data) - fmt.Printf("%x\n", sum[8:]) - - sh1 := sha1.Sum(data) - fmt.Printf("%x\n", sh1[10:]) -} - -func TestSize(t *testing.T) { - // 10 字节,重复100次,0.98KB - fmt.Printf("%.2f KB\n", 10.0*100/1024) - // 8 字节,重复100次,0.78KB - fmt.Printf("%.2f KB\n", 8.0*100/1024) -} - -func BenchmarkHash(b *testing.B) { - b.Run("md5", func(b *testing.B) { - data := []byte("some data to hash") - for i := 0; i < b.N; i++ { - _ = md5.Sum(data) - } - }) - b.Run("sha1", func(b *testing.B) { - data := []byte("some data to hash") - for i := 0; i < b.N; i++ { - _ = sha1.Sum(data) - } - }) - b.Run("sha256", func(b *testing.B) { - data := []byte("some data to hash") - for i := 0; i < b.N; i++ { - _ = sha256.Sum256(data) - } - }) - b.Run("sha512", func(b *testing.B) { - data := []byte("some data to hash") - for i := 0; i < b.N; i++ { - _ = sha512.Sum512(data) - } - }) -} - -func BenchmarkConcat(b *testing.B) { - b.Run("sha256", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = sha256.Sum256([]byte("some data to hash")) - } - }) - b.Run("sha256", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = sha256.Sum256([]byte("some data to hash" + "test")) - } - }) - -} diff --git a/misc/test/langDetect_test.go b/misc/test/langDetect_test.go deleted file mode 100644 index ed4a876..0000000 --- a/misc/test/langDetect_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - "testing" - - "github.com/Bistutu/whatlanggo" -) - -func TestLangdetect(t *testing.T) { - info := whatlanggo.Detect("中文") - fmt.Println("Language:", info.Lang.String(), " Script:", whatlanggo.Scripts[info.Script], " Confidence: ", info.Confidence) -} diff --git a/misc/test/statistics_test.go b/misc/test/statistics_test.go deleted file mode 100644 index 65c010a..0000000 --- a/misc/test/statistics_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "FluentRead/repo/db" - "FluentRead/utils" -) - -var ( - ctx = context.Background() -) - -// 统计未翻译的字符数 -func TestNotTransCountCharacter(t *testing.T) { - - listNotTranslated, err := db.ListNotTranslated(ctx) - assert.NoError(t, err) - count := 0 - for _, item := range listNotTranslated { - count += len(item.Source) - } - countInKB := float64(count) / 1024 - t.Logf("未翻译字符数:%d 字符,约 %.2f KB", count, countInKB) -} - -// 统计全部字符数 -func TestCountCharacter(t *testing.T) { - listNotTranslated, err := db.ListTrans(ctx) - assert.NoError(t, err) - count := 0 - for _, item := range listNotTranslated { - count += len(item.Source) - } - // 将字符总数转换为KB,假设每个字符1字节 - countInKB := float64(count) / 1024 - t.Logf("全部字符数:%d 字符,约 %.2f KB", count, countInKB) -} - -// 统计某网站的未翻译字符数 -func TestNotTransCountCharacterByLink(t *testing.T) { - link := utils.GetHost("https://platform.openai.com/") - // 查询网站对应的pageId - page, err := db.GetPageByLink(ctx, link) - assert.NoError(t, err) - trans, err := db.ListTrans(ctx) - - assert.NoError(t, err) - count := 0 - for _, item := range trans { - if item.PageId == page.ID { - count += len(item.Target) - } - } - countInKB := float64(count) / 1024 - t.Logf("%s 网站的字符数:%d 字符,约 %.2f KB", link, count, countInKB) -} diff --git a/misc/test/test_test.go b/misc/test/test_test.go deleted file mode 100644 index eef4e8c..0000000 --- a/misc/test/test_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "fmt" - "regexp" - "testing" - "time" -) - -func TestData(t *testing.T) { - year := 2020 - // 起始日期 - startDate := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC) - // 一直循环直到年份变成xx年 - for date := startDate; date.Year() == year; date = date.AddDate(0, 0, 1) { - //fmt.Println(date.Format("Jan 02, 2006")) - fmt.Println(date.Format("2006-01-02")) - } -} - -func TestCount(t *testing.T) { - for i := 0; i <= 50; i++ { - // 按照 "Compile Dependencies (i)" 的格式打印每个数字 - //fmt.Println(fmt.Sprintf("Test Dependencies (%d)", i)) - fmt.Println(fmt.Sprintf("测试依赖 Test (%d)", i)) - } -} - -// BenchmarkContainsNumber 对 ContainsNumber 函数进行基准测试 -func BenchmarkContainsNumber(b *testing.B) { - msg := "snjkanJNsjka21212" - // 在字符串中查找匹配项 - for i := 0; i < b.N; i++ { - re := regexp.MustCompile(`\d`) - // 调用函数并传入测试字符串 - re.MatchString(msg) - } -} - -func BenchmarkTest(b *testing.B) { - msg := "" - tmp := "snjkanJNsjka21212" - for i := 0; i < 100; i++ { - msg += tmp - } - fmt.Println(msg) - b.Run("1", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = msg - } - }) - b.Run("2", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = []byte(msg) - } - }) -} diff --git a/models/common.go b/models/common.go deleted file mode 100644 index bf2ee36..0000000 --- a/models/common.go +++ /dev/null @@ -1,45 +0,0 @@ -package models - -import ( - "container/list" - "crypto/sha1" - "encoding/hex" - "encoding/json" -) - -// Queue 队列 -type Queue struct { - list.List -} - -func (q *Queue) Pop() string { - front := q.List.Front() - v := front.Value.(string) - q.Remove(front) - return v -} - -func MapToBytes(m map[string]string) []byte { - bytes, _ := json.Marshal(m) - return bytes -} - -func MapToString(m map[string]string) string { - bytes, _ := json.Marshal(m) - return string(bytes) -} - -// Signature 返回 1/2 的 SHA-1 散列值 -func Signature(text string) string { - if text == "" { - return "" - } - hasher := sha1.New() - // 将输入文本转换为字节序列,并写入散列器。返回值:写入的字节数、错误 - _, _ = hasher.Write([]byte(text)) - // 计算散列值,得到字节序列。 - hashedBytes := hasher.Sum(nil) - // 将字节序列转换为十六进制编码的字符串 - hashedString := hex.EncodeToString(hashedBytes) - return hashedString[20:] -} diff --git a/models/constant/constant.go b/models/constant/constant.go deleted file mode 100644 index dd281b1..0000000 --- a/models/constant/constant.go +++ /dev/null @@ -1,16 +0,0 @@ -package constant - -const ( - SystemRoleMsg = `作为专业翻译,你的任务是将外语内容准确地翻译成中文。遵循以下规则: -- 翻译时要准确传达原文的事实和背景 -- 保留原段落格式和术语(如FLAC, JPEG),以及公司缩写(如Microsoft, Amazon) -- 人名原样保留 -- 图表格式保留,如“Figure 1: ”译为“图 1: ” -- 全角括号改为半角,左括号前、右括号后加空格。 -- 专业术语首次出现时附英文原词,如:“生成式 AI (Generative AI)” -- 减少使用中文句号和双引号 -你将翻译来自{{site}}网站,翻译的时候要考虑网站背景。 -下面请你开始按照序号逐行翻译,不要给出多余信息:` - CommonMsg = `汉化,逐行翻译来自{{site}}网站的内容: -` -) diff --git a/models/read.go b/models/read.go deleted file mode 100644 index b330c52..0000000 --- a/models/read.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -// ReadRequest 阅读请求 -type ReadRequest struct { - Page string `json:"page"` - TargetType uint `json:"target_type"` - HashList []string `json:"hash_list"` -} - -// ReadResponse 阅读响应,hash:target -type ReadResponse struct { - Data map[string]string `json:"data"` -} diff --git a/models/result.go b/models/result.go deleted file mode 100644 index 32e824e..0000000 --- a/models/result.go +++ /dev/null @@ -1,24 +0,0 @@ -package models - -// Result 通用响应结构 -type Result struct { - Code int `json:"code"` - Msg string `json:"msg"` - Data interface{} `json:"Data"` -} - -func Success(data interface{}) *Result { - return &Result{ - Code: 0, - Msg: "ok", - Data: data, - } -} - -func Fail(msg string) *Result { - return &Result{ - Code: -1, - Msg: msg, - Data: nil, - } -} diff --git a/models/text.go b/models/text.go deleted file mode 100644 index 0d67307..0000000 --- a/models/text.go +++ /dev/null @@ -1,43 +0,0 @@ -package models - -import ( - "gorm.io/gorm" -) - -// cache 键的规范:域名:哈希值 - -// Translation 翻译文本模型 -type Translation struct { - Source string `json:"source" gorm:"column:source;type:text";` // 原文 - Target string `json:"target" gorm:"column:target;type:text"` // 译文 - Hash string `json:"hash" gorm:"column:hash;type:char(20);unique;"` // 哈希值 - Translated bool `json:"translated" gorm:"column:translated;type:tinyint(1);default:0"` // 是否已经翻译 - TargetType uint `json:"target_type" gorm:"column:target_type"` // 0 表示未知,1表示中文 - PageId uint `json:"page_id" gorm:"column:page_id;type:int;default:0;index;comment:0表示未知"` // 页面 id - gorm.Model -} - -// Page 页面信息 -type Page struct { - Link string `json:"link" gorm:"column:link;type:varchar(512);unique;"` // 页面链接 - Describe string `json:"describe" gorm:"column:describe;type:varchar(256)"` // 页面描述信息 - gorm.Model -} - -// BatchTransToMap 将批量翻译结果转换为 map -func BatchTransToMap(transModels []*Translation) map[string]string { - rs := make(map[string]string, len(transModels)) - for _, v := range transModels { - rs[v.Hash] = v.Target - } - return rs -} - -// PageToMap 将页面数组转换为 map,key:页面链接,value:页面更新时间的哈希值 -func PageToMap(pages []*Page) map[string]string { - rs := make(map[string]string, len(pages)) - for _, v := range pages { - rs[v.Link] = Signature(v.UpdatedAt.String()) - } - return rs -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8a0768d --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "流畅阅读", + "description": "拥有基于上下文语境的人工智能翻译引擎,为网站提供更加友好的翻译,让所有人都能够拥有基于母语般的阅读体验", + "private": true, + "version": "0.0.2", + "type": "module", + "scripts": { + "dev": "wxt", + "dev:firefox": "wxt -b firefox", + "build": "wxt build", + "build:firefox": "wxt build -b firefox", + "zip": "wxt zip", + "zip:firefox": "wxt zip -b firefox", + "compile": "vue-tsc --noEmit", + "postinstall": "wxt prepare" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "crypto-js": "^4.2.0", + "element-plus": "^2.6.2", + "franc-min": "^6.2.0", + "js-beautify": "^1.15.1", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.1", + "typescript": "^5.3.3", + "vite": "^5.2.7", + "vue-tsc": "^2.0.6", + "wxt": "^0.17.0" + } +} diff --git a/plugin/ParseSentence.js b/plugin/ParseSentence.js deleted file mode 100644 index b0a8739..0000000 --- a/plugin/ParseSentence.js +++ /dev/null @@ -1,68 +0,0 @@ -// ==UserScript== -// @name ParseSentence -// @namespace http://tampermonkey.net/ -// @version 2024-01-11 -// @description 解析获取页面上所有句子 -// @author ThinkStu -// @match *://*/* -// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com -// @grant none -// ==/UserScript== - -(function () { - 'use strict'; - - let ctrlPressed = false; - let hoverTimer; - - document.addEventListener('keydown', event => { - if (event.key === "Control") ctrlPressed = true; - }); - - document.addEventListener('keyup', event => { - if (event.key === "Control") ctrlPressed = false - }); - - // 当浏览器或标签页失去焦点时,重置 ctrlPressed - window.addEventListener('blur', () => ctrlPressed = false) - - - // 去重 set - let sentenceSet = new Set(); - - // 增加鼠标监听事件 - document.addEventListener('mousemove', (event) => { - if (!ctrlPressed) return; - - clearTimeout(hoverTimer); // 清除之前的计时器 - hoverTimer = setTimeout(() => { - let hoveredElement = event.target; - let textContent = ''; - - // 去重判断 - if (sentenceSet.has(hoveredElement)) return; - - // 如果存在子节点 - if (hoveredElement.childNodes.length > 0) { - // 遍历所有子节点 - hoveredElement.childNodes.forEach(node => { - if (node.nodeType === Node.TEXT_NODE) { - // 如果是文本节点,添加其文本 - textContent += node.textContent.trim() + ' '; - } else if (node.nodeType === Node.ELEMENT_NODE && node.innerText) { - textContent += node.innerText.trim() + ' '; - } - }); - } else { // 如果没有子节点,直接获取元素的文本 - textContent = hoveredElement.textContent.trim(); - } - - // 检查换行符 - if (textContent && textContent.split("\n").length === 1) { - sentenceSet.add(hoveredElement); - console.log("Hovered Text: ", textContent); - } - }, 50) - }); -})(); - diff --git a/plugin/PrintText.js b/plugin/PrintText.js deleted file mode 100644 index 8d224b4..0000000 --- a/plugin/PrintText.js +++ /dev/null @@ -1,227 +0,0 @@ -// ==UserScript== -// @name 打印英文文本 -// @namespace https://fr.unmeta.cn/ -// @version 0.1 -// @description 获取页面上的所有英文文本,并在控制台打印 -// @author ThinkStu -// @icon [icon URL] -// @match *://*/* -// @grant GM_setValue -// @grant GM_getValue -// @grant GM_listValues -// @grant GM_deleteValue -// @run-at document-idle -// ==/UserScript== - -// region 变量与常量 -let textSet = new Set(); -// 输出文本集信息 + 防抖 -const debouncedEcho = debounce(echo, 500); -// 存储的Key -const hostKey = "host_key"; -let url = new URL(location.href.split('?')[0]); - -// endregion - -(function () { - 'use strict'; - - setTimeout(() => { - parseDfs(document.body); - // 输出文本集信息 - debouncedEcho(); - - // 使用 MutationObserver 监听 DOM 变化,配置和启动观察器 - const observer = new MutationObserver(function (mutations, obs) { - mutations.forEach(mutation => { - let node = mutation.target; - - // 处理每个变更记录 - if (["div", "section","main","tbody","tr","td","button", "svg", "span", "nav", "body", "label"].includes(node.tagName.toLowerCase())) { - parseDfs(node); - debouncedEcho(); - } - }); - }); - observer.observe(document.body, {childList: true, subtree: true}); - }, 2000); // 延迟时间设置为2000毫秒(2秒) - - // 快捷键 F3 清空 Set 缓存 - document.addEventListener('keydown', function (event) { - if (event.key === 'F3') { - // 清空所有缓存 - // textSet.clear(); - let listValues = GM_listValues(); - listValues.forEach(e => { - GM_deleteValue(e) - }) - console.log('清空所有文本缓存🥹'); - } - }); -})(); - -// 递归提取节点的文本内容 -function parseDfs(node) { - // 跳过一些域名 - if (url.host.match(/(www.baidu|www.google)/)) { - return; - } - - switch (node.nodeType) { - // 元素节点 - case Node.ELEMENT_NODE: - // TODO 限定条件,跳过不必要的 node - if (isSkip(node) || ["head", "picture", "script", "style", "img", "noscript"].includes(node.tagName.toLowerCase())) { - // console.log("忽略节点: ", node); - return; - } - if (["input", "textarea"].includes(node.tagName.toLowerCase())) { - processInput(node); - } - if (node.hasAttribute("aria-label")) { - processAriaLabel(node) - } - break - // 文本节点 - case Node.TEXT_NODE : - parseText(node); - } - - let child = node.firstChild; - while (child) { - parseDfs(child); - child = child.nextSibling; - } -} - -// 正则表达式辅助跳过 -const regexMBKB = /^\d+(\.\d+)?(MB|KB|GB|TB)$/; - -function parseText(node) { - let text = node.textContent.replace(/\u00A0/g, ' ').trim(); - if (regexMBKB.test(text)) { - return; - } - if (text.length > 0 && withoutChinese(text) && isEnglish(text)) { - // 从 GM 中取出 host 对应的值 - process(text); - } -} - -// 真正处理文本 -function process(text) { - let hostValue = GM_getValue(url.host); - let host; - if (!Array.isArray(hostValue)) { - host = new Set(); - } else { - host = new Set(hostValue); - } - host.add(text); - // 将 Set 转换为数组以存储 - GM_setValue(url.host, Array.from(host)); -} - -function processInput(node) { - let placeholder = node.placeholder.replace(/\u00A0/g, ' ').trim(); - let value = node.value.replace(/\u00A0/g, ' ').trim(); - - if (placeholder.length > 0 && withoutChinese(placeholder) && isEnglish(placeholder)) { - process(placeholder); - } - if (value.length > 0 && withoutChinese(value) && isEnglish(value)) { - process(value); - } -} - -// read:处理 aria-label 属性 -function processAriaLabel(node) { - let ariaLabel = node.getAttribute('aria-label').replace(/\u00A0/g, ' ').trim(); - if (ariaLabel) { - if (ariaLabel.length > 0 && withoutChinese(ariaLabel)) { - process(ariaLabel); - } - } -} - -// endregion - -// region 通用函数 - -// read:判断具有特殊属性的节点是否应该被跳过 -function isSkip(node) { - - return node.hasAttribute("data-message-author-role") - // mod模组系列 - || node.classList.contains("mod-image") - // || node.classList.contains("tile-desc") - || node.classList.contains("desc") - // 其余系列 - || node.classList.contains("post-layout") - || node.hasAttribute("data-post-id") - || node.classList.contains("s-post-summary--content") - || node.classList.contains("d-block") - || node.classList.contains("s-prose") - || node.classList.contains("question-hyperlink") - || node.classList.contains("user-info") - || node.classList.contains("js-post-body") - || node.id === "inline_related_var_a_less" - || node.id === "hot-network-questions" - // coze - || node.classList.contains("WGuG2UMJJ8wbd0zOu7JN") - || node.classList.contains("semi-typography") - || node.classList.contains("flow-markdown-body") - || node.classList.contains("jwzzTyL0ME4eVCKuxpDL") - || node.classList.contains("WsIBTuSuHYIPuetXwr1f") - || node.classList.contains("kJSEgautwioJ9kmI18b8") - -} - - -// 将Set转换为数组,对数组进行字母排序(忽略大小写),并输出排序后的数组 -function echo() { - let listValues = GM_listValues(); - listValues.forEach(key => { - let value = GM_getValue(key); - // 打印键和值 - // if (key.match(/.*(\.)?\..*/)) { - // 在哪个页面就打印哪个页面的数据 - if (key === url.host) { - console.log(key); - console.log(value); - } - - }) -} - -// 判断字符串是非中文 -function withoutChinese(text) { - return !/[\u4e00-\u9fa5]/.test(text); -} - -// 判断字符串是英文 -function isEnglish(text) { - for (let i = 0; i < text.length; i++) { - let v = text.charCodeAt(i); - if ((v >= 'a'.charCodeAt(0) && v <= 'z'.charCodeAt(0)) || (v >= 'A'.charCodeAt(0) && v <= 'Z'.charCodeAt(0))) { - return true; - } - } - return false; -} - -// 防抖函数 -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -}; - - -// endregion \ No newline at end of file diff --git a/plugin/select_range.js b/plugin/select_range.js deleted file mode 100644 index d70c7ae..0000000 --- a/plugin/select_range.js +++ /dev/null @@ -1,111 +0,0 @@ -// ==UserScript== -// @name demo -// @namespace http://tampermonkey.net/ -// @version 2024-01-28 -// @description try to take over the world! -// @author You -// @match *://*/* -// @grant GM_setValue -// @grant GM_getValue -// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== -// ==/UserScript== - - -let hoverTimer; - -// prune set -let pruneSet = new Set(); - -(function () { - 'use strict'; - // 为整个文档添加一个鼠标移动事件监听器 - clearTimeout(hoverTimer); // 清除计时器 - hoverTimer = setTimeout(() => { - document.addEventListener('mousemove', function (event) { - let paragraph = getMouseOverParagraph(event.clientX, event.clientY); - if (pruneSet.has(paragraph)) return - pruneSet.add(paragraph) - if (paragraph) { - console.log('找到段落:', paragraph); - } - }); - }, 50); - -})(); - -// 定义一个函数,根据鼠标指针的位置获取当前鼠标悬停的段落元素 -function getMouseOverParagraph(clientX2, clientY2) { - // 从鼠标指针位置创建一个文本范围 - let range = getRangeFromPoint(clientX2, clientY2); - // 如果范围为null,即没有找到元素,则返回 - if (range == null) - return; - - // 定义一个检查非文本元素的函数 - let checkTheUnTextElement = () => { - // 从指定的点获取最上层的元素 - let pointElement = document.elementFromPoint( - clientX2, - clientY2 - ); - // 如果没有找到元素,返回 - if (!pointElement) - return; - // 检查是否有实际的内部元素(可能在shadow DOM内) - let realInnerElement = findElementInShadow( - pointElement, - clientX2, - clientY2 - ); - // 检查找到的元素是否是BUTTON,如果是,返回这个BUTTON,如果不是,返回它的块级父元素 - return realInnerElement === pointElement ? pointElement.nodeName === "BUTTON" ? pointElement : void 0 : getBlockParentNode(realInnerElement); - }, - // 定义一个检查文本节点的函数 - checkTheTextNode = () => { - // 调整range的开始和结束位置,以包含开始容器的文本节点 - range.setStartBefore(range.startContainer), range.setEndAfter(range.startContainer); - // 获取当前范围的边界矩形 - let rect = range.getBoundingClientRect(); - // 检查鼠标位置是否在文本范围内,如果是,返回这个范围的块级父元素 - if (!(rect.left > clientX2 || rect.right < clientX2 || rect.top > clientY2 || rect.bottom < clientY2)) - return getBlockParentNode(range.startContainer); - }, - // 定义一个变量来存储找到的元素 - findedElement; - // 检查开始容器的节点类型是否为文本节点,调用相应的检查函数 - return range.startContainer.nodeType !== Node.TEXT_NODE ? findedElement = checkTheUnTextElement() : findedElement = checkTheTextNode(), findedElement; -} - -function getBlockParentNode(startNode) { - return startNode.nodeType === Node.TEXT_NODE ? startNode.parentNode : startNode; -} - -function getRangeFromPoint(x4, y4) { - if (document.caretPositionFromPoint) { - let position = document.caretPositionFromPoint(x4, y4); - if (position) { - let range = document.createRange(), offsetNode = position.offsetNode; - if (!offsetNode || offsetNode.nodeType !== Node.TEXT_NODE) - return null; - range.setStart(offsetNode, position.offset), range.setEnd(offsetNode, position.offset); - return range; - } - return null; - } else - return document.caretRangeFromPoint ? document.caretRangeFromPoint(x4, y4) : null; -} - - -function findElementInShadow(ele, x4, y4) { - let findCount = 0, finder = (ele2, x5, y5, preShadow) => { - if (++findCount > 100 || preShadow === ele2) - return ele2; - let innerShadowDom = ele2.shadowRoot; - if (!innerShadowDom || typeof innerShadowDom.elementFromPoint != "function") - return ele2; - let innerDom = innerShadowDom.elementFromPoint(x5, y5); - return innerDom ? finder(innerDom, x5, y5, ele2) : ele2; - }; - return finder(ele, x4, y4); -} - diff --git a/public/icon/128.png b/public/icon/128.png new file mode 100644 index 0000000..68e2d8d Binary files /dev/null and b/public/icon/128.png differ diff --git a/public/icon/16.png b/public/icon/16.png new file mode 100644 index 0000000..036fb8b Binary files /dev/null and b/public/icon/16.png differ diff --git a/public/icon/192.png b/public/icon/192.png new file mode 100644 index 0000000..eaa8fbf Binary files /dev/null and b/public/icon/192.png differ diff --git a/public/icon/256.png b/public/icon/256.png new file mode 100644 index 0000000..7fa93c2 Binary files /dev/null and b/public/icon/256.png differ diff --git a/public/icon/32.png b/public/icon/32.png new file mode 100644 index 0000000..9d384e1 Binary files /dev/null and b/public/icon/32.png differ diff --git a/public/icon/48.png b/public/icon/48.png new file mode 100644 index 0000000..057df72 Binary files /dev/null and b/public/icon/48.png differ diff --git a/public/icon/96.png b/public/icon/96.png new file mode 100644 index 0000000..a45df6c Binary files /dev/null and b/public/icon/96.png differ diff --git a/repo/cache/redis.go b/repo/cache/redis.go deleted file mode 100644 index cb8a1c8..0000000 --- a/repo/cache/redis.go +++ /dev/null @@ -1,87 +0,0 @@ -package cache - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/go-redis/redis/v8" - - "FluentRead/utils" -) - -const ( - dsnFormat = "127.0.0.1:%s" -) - -var ( - rdb *redis.Client - expiration = 24 * time.Hour // 默认缓存 1 天 - //expiration = time.Millisecond // 默认缓存 1 毫秒 -) - -func init() { - rdb = redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf(dsnFormat, utils.GetEnvDefault("REDIS_PORT", "16379")), // redis 服务端地址 - Password: utils.GetEnvDefault("REDIS_PASSWORD_FR", "SzW7fh2Fs5d2ypwT"), // redis 密码 - DB: 0, - }) -} - -// SetKey 默认过期时间 24 小时 -func SetKey(ctx context.Context, key string, value interface{}) error { - // TODO 测试 - expiration = 1 * time.Second - return rdb.Set(ctx, key, value, expiration).Err() -} - -// SetKeyWithTimeout 自定义过期时间 -func SetKeyWithTimeout(ctx context.Context, key string, timeout time.Duration, value interface{}) error { - // TODO 测试 - timeout = 1 * time.Second - - return rdb.Set(ctx, key, value, timeout).Err() -} - -// SetKeyNotExpiration 永不过期 -func SetKeyNotExpiration(ctx context.Context, key string, value interface{}) error { - return rdb.Set(ctx, key, value, 0).Err() -} - -func GetKey(ctx context.Context, key string) (string, error) { - val, err := rdb.Get(ctx, key).Result() - // redis.Nil 不是错误,表示 key 不存在 - if !errors.Is(err, redis.Nil) && err != nil { - return "", err - } - - return val, nil -} - -func MGet(ctx context.Context, keys ...string) ([]interface{}, error) { - if len(keys) == 0 { - return nil, errors.New("keys is empty") - } - - return rdb.MGet(ctx, keys...).Result() -} - -func MSet(ctx context.Context, kv sync.Map) error { - pipeline := rdb.Pipeline() - kv.Range(func(k, v any) bool { - pipeline.Set(ctx, k.(string), v, expiration) - return true - }) - // 不关注单个命令的执行结果,只关注 pipeline 执行的结果 - if _, err := pipeline.Exec(ctx); err != nil { - return err - } - return nil -} - -// RemoveKey 删除缓存 -func RemoveKey(ctx context.Context, key string) error { - return rdb.Del(ctx, key).Err() -} diff --git a/repo/cache/redis_test.go b/repo/cache/redis_test.go deleted file mode 100644 index 817b3b7..0000000 --- a/repo/cache/redis_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package cache - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSet(t *testing.T) { - ctx := context.Background() - - msg := []string{"test1", "value1"} - err := SetKey(ctx, msg[0], msg[1]) - assert.NoError(t, err) - - rs, err := GetKey(ctx, msg[0]) - assert.NoError(t, err) - assert.Equal(t, msg[1], rs) -} diff --git a/repo/db/misc.go b/repo/db/misc.go deleted file mode 100644 index 0a0bc3d..0000000 --- a/repo/db/misc.go +++ /dev/null @@ -1,18 +0,0 @@ -package db - -import ( - "context" - - "FluentRead/models" -) - -// GetAllByPage 获取某个页面的所有短语 -func GetAllByPage(ctx context.Context, page string) ([]*models.Translation, error) { - var translations []*models.Translation - // 子查询 - err := db.Where("page_id = (SELECT id FROM pages WHERE link = ?)", page).Find(&translations).Error - if err != nil { - return nil, err - } - return translations, nil -} diff --git a/repo/db/mysql.go b/repo/db/mysql.go deleted file mode 100644 index 4321df5..0000000 --- a/repo/db/mysql.go +++ /dev/null @@ -1,36 +0,0 @@ -package db - -import ( - "fmt" - - "gorm.io/driver/mysql" - "gorm.io/gorm" - - "FluentRead/misc/log" - "FluentRead/models" - "FluentRead/utils" -) - -const ( - dsnFormat = "%s:%s@tcp(127.0.0.1:3306)/fluent_read?charset=utf8mb4&parseTime=True&loc=Local" -) - -var db *gorm.DB - -func init() { - dsn := fmt.Sprintf(dsnFormat, - utils.GetEnvDefault("MYSQL_USERNAME_FR", "fluent_read"), - utils.GetEnvDefault("MYSQL_PASSWORD_FR", "kwaRhpptf57mfi7k"), - ) - open, err := gorm.Open(mysql.Open(dsn)) - if err != nil { - log.Errorf("数据库连接失败:%v", err) - panic(err) - } - db = open - // 自动创建表 - err = db.AutoMigrate(&models.Translation{}, &models.Page{}) - if err != nil { - panic(err) - } -} diff --git a/repo/db/page.go b/repo/db/page.go deleted file mode 100644 index 9346c8d..0000000 --- a/repo/db/page.go +++ /dev/null @@ -1,74 +0,0 @@ -package db - -import ( - "context" - "errors" - - "github.com/go-sql-driver/mysql" - "gorm.io/gorm" - "gorm.io/gorm/clause" - - "FluentRead/misc/log" - "FluentRead/models" -) - -// InsertPage 插入页面信息,如果已存在则停止操作,无论如何最终会返回对应的 pageId -func InsertPage(ctx context.Context, page *models.Page) (uint, error) { - err := db.Create(page).Error - if err != nil { - // 尝试将错误断言为 *mysql.MySQLError - var mysqlErr *mysql.MySQLError - if ok := errors.As(err, &mysqlErr); ok && mysqlErr.Number != 1062 { - return 0, err - } - - // 如果是因为唯一键冲突,则尝试获取已存在记录的 ID - var existingPage *models.Page - if err := db.Where("link = ?", page.Link).First(&existingPage).Error; err != nil { - log.Errorf("查询已存在的记录失败: %v", err) - return 0, err - } - return existingPage.ID, nil - } - return page.ID, nil -} - -func InsertOrUpdatePage(ctx context.Context, page *models.Page) (uint, error) { - tx := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "link"}}, - DoUpdates: clause.AssignmentColumns([]string{"updated_at"}), - }).Create(page) - return page.ID, tx.Error -} - -// BatchInsertPageInfo 批量插入页面信息,如果已存在则不插入 -func BatchInsertPageInfo(ctx context.Context, pages []*models.Page) error { - // 开启事务 - return db.Transaction(func(tx *gorm.DB) error { - for _, page := range pages { - err := tx.Create(page).Error - // 如果是唯一键冲突,不报错 - if !errors.Is(err, gorm.ErrDuplicatedKey) && err != nil { - return err - } - } - return nil - }) -} - -// GetPageByLink 按 link 获取页面信息 -func GetPageByLink(ctx context.Context, link string) (*models.Page, error) { - var page models.Page - return &page, db.Where("link = ?", link).First(&page).Error -} - -// GetPageById 按 id 获取页面信息 -func GetPageById(ctx context.Context, id uint) (*models.Page, error) { - var page models.Page - return &page, db.First(&page, id).Error -} - -func ListPages(ctx context.Context) ([]*models.Page, error) { - var pages []*models.Page - return pages, db.Find(&pages).Error -} diff --git a/repo/db/page_test.go b/repo/db/page_test.go deleted file mode 100644 index d1646a8..0000000 --- a/repo/db/page_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package db - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "FluentRead/models" -) - -func TestInsertPage(t *testing.T) { - ctx := context.Background() - pageId, err := InsertPage(ctx, &models.Page{Link: "www.baidu.com"}) - assert.NoError(t, err) - t.Log(pageId) -} diff --git a/repo/db/trans.go b/repo/db/trans.go deleted file mode 100644 index 3fdb8ba..0000000 --- a/repo/db/trans.go +++ /dev/null @@ -1,90 +0,0 @@ -package db - -import ( - "context" - - "gorm.io/gorm/clause" - - "FluentRead/models" -) - -/*插入/更新*/ - -// InsertTrans 插入一条翻译记录,冲突时不更新任何字段 -func InsertTrans(ctx context.Context, model *models.Translation) (err error) { - // 跳过一些不需要翻译的短语 - //if utils.IsContain(model.Source, []string{"153 results", "T", "ThinkStu"}) { - // return nil - //} - return db.Clauses(clause.OnConflict{ - UpdateAll: false, - }).Create(model).Error -} - -// UpdateTrans 更新一条记录 -func UpdateTrans(ctx context.Context, model *models.Translation) error { - return db.Updates(model).Error -} - -// BatchUpdateTrans 批量更新记录 -func BatchUpdateTrans(ctx context.Context, models []*models.Translation) error { - // 启动事务 - tx := db.WithContext(ctx).Begin() - if tx.Error != nil { - return tx.Error - } - // 遍历更新 - for _, model := range models { - if err := tx.Updates(model).Error; err != nil { - tx.Rollback() // 如果有错误发生,回滚事务 - return err - } - } - return tx.Commit().Error // 提交事务 -} - -// BatchInsert 批量插入记录,发生冲突时只更新 updated_at 字段 -func BatchInsert(ctx context.Context, models []*models.Translation) error { - // 发生冲突时,只更新 updated_at 字段 - return db.Clauses(clause.OnConflict{ - DoUpdates: clause.AssignmentColumns([]string{"updated_at"}), - UpdateAll: false, - }).CreateInBatches(models, 100).Error -} - -/*查询*/ - -// GetOne 获取一条记录 -func GetOne(ctx context.Context, hashCode string) (model *models.Translation, err error) { - err = db.Where("hash = ?", hashCode).First(&model).Error - if err != nil { - return nil, err - } - return model, nil -} - -// BatchGet 批量获取记录 -func BatchGet(ctx context.Context, hashCodes []string) (models []*models.Translation, err error) { - err = db.Where("hash in (?)", hashCodes).Find(&models).Error - if err != nil { - return nil, err - } - return models, nil -} - -// ListNotTranslated 获取还未翻译的短语 -func ListNotTranslated(ctx context.Context) ([]*models.Translation, error) { - var models []*models.Translation - return models, db.Where("translated = ?", 0).Order("id").Find(&models).Error -} - -// ListTrans 获取所有短语 -func ListTrans(ctx context.Context) ([]*models.Translation, error) { - var models []*models.Translation - return models, db.Order("id").Find(&models).Error -} - -// RemoveTrans 删除 -func RemoveTrans(ctx context.Context, trans *models.Translation) error { - return db.Delete(trans).Error -} diff --git a/repo/db/trans_test.go b/repo/db/trans_test.go deleted file mode 100644 index b570384..0000000 --- a/repo/db/trans_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package db - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "gorm.io/gorm" - - "FluentRead/models" -) - -func TestInsertAndGet(t *testing.T) { - ctx := context.Background() - - text := "Overview" - target := "简介" - signature := "demo_signature" - - model := &models.Translation{ - Model: gorm.Model{ - ID: 0, - }, - Hash: signature, - Source: text, - TargetType: 1, - Target: target, - } - err := InsertTrans(ctx, model) - assert.NoError(t, err) - - one, err := GetOne(ctx, signature) - assert.NoError(t, err) - assert.Equal(t, target, one.Target) -} - -func TestListNotTranslated(t *testing.T) { - list, err := ListNotTranslated(context.Background()) - assert.NoError(t, err) - t.Log(list) -} diff --git a/translation/alibaba.go b/translation/alibaba.go deleted file mode 100644 index 1f4c7db..0000000 --- a/translation/alibaba.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package translation 阿里翻译 -package translation - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strings" - - alimt20181012 "github.com/alibabacloud-go/alimt-20181012/v2/client" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - util "github.com/alibabacloud-go/tea-utils/v2/service" - "github.com/alibabacloud-go/tea/tea" -) - -func AliTrans(raw string) (string, error) { - user := tea.String(os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")) - pass := tea.String(os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")) - client, err := CreateClient(user, pass) - if err != nil { - panic(err) - } - - translateGeneralRequest := &alimt20181012.TranslateGeneralRequest{ - FormatType: tea.String("text"), - SourceLanguage: tea.String("en"), - TargetLanguage: tea.String("zh"), - SourceText: tea.String(raw), - Context: tea.String("程序员/开发者/Developer"), - Scene: tea.String("general"), - } - runtime := &util.RuntimeOptions{} - translatedText, tryErr := func() (string, error) { - defer func() { - if r := tea.Recover(recover()); r != nil { - err = r - } - }() - msg, err := client.TranslateGeneralWithOptions(translateGeneralRequest, runtime) - if err != nil { - return "", err - } - return *msg.Body.Data.Translated, nil - }() - - if tryErr != nil { - - var error = &tea.SDKError{} - var _t *tea.SDKError - if errors.As(tryErr, &_t) { - error = _t - } - // 错误 message - fmt.Println(tea.StringValue(error.Message)) - // 诊断地址 - var data interface{} - d := json.NewDecoder(strings.NewReader(tea.StringValue(error.Data))) - d.Decode(&data) - if m, ok := data.(map[string]interface{}); ok { - recommend, _ := m["Recommend"] - fmt.Println(recommend) - } - _, err := util.AssertAsString(error.Message) - if err != nil { - return "", err - } - } - return translatedText, nil -} - -func CreateClient(accessKeyId *string, accessKeySecret *string) (_result *alimt20181012.Client, _err error) { - config := &openapi.Config{ - AccessKeyId: accessKeyId, - AccessKeySecret: accessKeySecret, - } - config.Endpoint = tea.String("mt.aliyuncs.com") - _result = &alimt20181012.Client{} - _result, _err = alimt20181012.NewClient(config) - return _result, _err -} diff --git a/translation/bing.go b/translation/bing.go deleted file mode 100644 index 35a3b0e..0000000 --- a/translation/bing.go +++ /dev/null @@ -1,9 +0,0 @@ -package translation - -import "github.com/hwfy/translate" - -// BingTrans 必应翻译,发生错误返回空字符串 -func BingTrans(origin string) string { - // 任意语言转中文 - return translate.ToSimplifiedByBing(origin) -} diff --git a/translation/common_test.go b/translation/common_test.go deleted file mode 100644 index ec024fb..0000000 --- a/translation/common_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package translation - -import ( - "fmt" - "testing" -) - -var text = "Hello, World!" - -func TestGoogleTrans(t *testing.T) { - - for i := 0; i < 10; i++ { - result, _ := GoogleTrans(text) - fmt.Println(result) - } -} - -func TestBingTrans(t *testing.T) { - for i := 0; i < 10; i++ { - result := BingTrans(text) - fmt.Println(result) - } -} - -func TestAliTrans(t *testing.T) { - for i := 0; i < 10; i++ { - result, _ := AliTrans(text) - fmt.Println(result) - } -} diff --git a/translation/google.go b/translation/google.go deleted file mode 100644 index 4b180a4..0000000 --- a/translation/google.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package translation 谷歌翻译 -package translation - -import gt "github.com/bas24/googletranslatefree" - -// GoogleTrans 谷歌翻译 -func GoogleTrans(origin string) (string, error) { - result, err := gt.Translate(origin, "auto", "zh") - return result, err -} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dae5c41 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.wxt/tsconfig.json", +} diff --git a/utils/common.go b/utils/common.go deleted file mode 100644 index 5f20e7e..0000000 --- a/utils/common.go +++ /dev/null @@ -1,71 +0,0 @@ -// Package utils spider 通用工具 -package utils - -import ( - "net/url" - "os" - "strings" - "unicode" - - "FluentRead/misc/log" -) - -// IsContain 判断字符串是否在字符串数组中 -func IsContain(str string, strs []string) bool { - for _, v := range strs { - if v == str { - return true - } - } - return false -} - -// GetHost 根据 string 获取链接的host -func GetHost(link string) string { - parsedUrl, err := url.Parse(link) - if err != nil { - log.Errorf("链接解析失败: %v", err) - return "" - } - return parsedUrl.Host -} - -// IsNonChinese 检查字符串是非中文 -func IsNonChinese(text string) bool { - for _, r := range text { - if unicode.Is(unicode.Han, r) { - return false - } - } - return true -} - -// IsEnglish 判断是否包含英文字符 -func IsEnglish(text string) bool { - for _, v := range text { - if (v >= 'a' && v <= 'z') || (v >= 'A' && v <= 'Z') { - return true - } - } - return false -} - -// IsAllEnglishAndNum 检查是否为纯[英文+数字] -func IsAllEnglishAndNum(fragment string) bool { - fragment = strings.TrimSpace(fragment) - for _, v := range fragment { - if !((v >= 'a' && v <= 'z') || (v >= 'A' && v <= 'Z') || (v >= '0' && v <= '9')) { - return false - } - } - return true -} - -// GetEnvDefault 获取环境变量,如果为空则返回备用值 -func GetEnvDefault(env string, backup string) string { - value := os.Getenv(env) - if value == "" { - return backup - } - return value -} diff --git a/utils/common_test.go b/utils/common_test.go deleted file mode 100644 index 9c69ef2..0000000 --- a/utils/common_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package utils - -import ( - "testing" -) - -// check是否为英语单词性能测试 -func BenchmarkCheck(b *testing.B) { - msg := "😃check" - b.Run("spider", func(b *testing.B) { - for i := 0; i < b.N; i++ { - IsEnglish(msg) - } - }) -} diff --git a/utils/error.go b/utils/error.go deleted file mode 100644 index 10edbd7..0000000 --- a/utils/error.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package utils 常见错误 -package utils - -import ( - "github.com/gin-gonic/gin" - - "FluentRead/misc/log" - "FluentRead/models" -) - -const ( - badRequest = "不合法的请求" - systemError = "系统错误,请稍后重试" -) - -// CallbackBadRequest 不合法的请求 -func CallbackBadRequest(c *gin.Context, err error) { - log.Error(badRequest, err) - c.JSON(400, models.Fail(badRequest)) -} - -// CallbackSystemError 系统错误 -func CallbackSystemError(c *gin.Context, err error) { - log.Error(systemError, err) - c.JSON(500, models.Fail(systemError)) -} diff --git a/utils/http.go b/utils/http.go deleted file mode 100644 index b0ee0d3..0000000 --- a/utils/http.go +++ /dev/null @@ -1,45 +0,0 @@ -package utils - -import ( - "io" - "net/http" -) - -const ( - contentType = "Content-Type" - userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" -) - -var client *http.Client -var clientNoRedirect *http.Client - -func init() { - client = &http.Client{} - clientNoRedirect = &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse // 返回此错误阻止重定向 - }, - } -} - -// Get 模拟客户端发送 GET 请求 -func Get(link string) (*http.Response, error) { - request, _ := http.NewRequest("GET", link, nil) - request.Header.Set(contentType, userAgent) - request.Header.Set("user-agent", userAgent) - return client.Do(request) -} - -func Post(link string, data io.Reader) (*http.Response, error) { - request, _ := http.NewRequest("POST", link, data) - request.Header.Set(contentType, userAgent) - return client.Do(request) -} - -func FetchRedirectURL(link string) (string, error) { - resp, err := clientNoRedirect.Get(link) - if err != nil { - return "", err - } - return resp.Header.Get("Location"), nil -} diff --git a/wxt.config.ts b/wxt.config.ts new file mode 100644 index 0000000..afda6d3 --- /dev/null +++ b/wxt.config.ts @@ -0,0 +1,17 @@ +import {defineConfig} from 'wxt'; +import vue from '@vitejs/plugin-vue'; + +// See https://wxt.dev/api/config.html +export default defineConfig({ + imports: { + addons: { + vueTemplate: true, + }, + }, + vite: () => ({ + plugins: [vue()], + }), + manifest: { + permissions: ['storage'], + }, +});