Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/asadahimeka_pixiv-viewer.json
T

1 line
17 KiB
JSON

{"repo": "asadahimeka/pixiv-viewer", "files": {"src/utils/translate/index.js": {"2f6c21c9382c42fd83c7a2aa0932a38a7acfcdc4": {"t": 1785674137.710803, "n": 11051, "h": "1979cbc5f38c7add", "c": "import { Dialog } from 'vant'\nimport { i18n } from '@/i18n'\nimport { SILICON_CLOUD_API_KEY } from '@/consts'\nimport { loadScript } from '@/utils'\n\nexport async function loadKISSTranslator(isAutoLoad = false) {\n if (!isAutoLoad && !localStorage.getItem('PXV_KISST_CFMED')) {\n const res = await Dialog.confirm({\n title: '加载 KISS Translator 脚本',\n message: '提示:如果已安装 KISS Translator 浏览器扩展或用户脚本则无需加载。可在偏好设置中打开自动加载 KISS Translator 脚本。',\n lockScroll: false,\n closeOnPopstate: true,\n cancelButtonText: '取消',\n confirmButtonText: '加载',\n }).catch(() => 'cancel')\n if (res != 'confirm') return\n localStorage.setItem('PXV_KISST_CFMED', '1')\n }\n if (document.querySelector('#kiss-translator')) {\n return\n }\n await loadScript('/kiss-translator/KISS-Translator.min.js')\n if (isAutoLoad) {\n setTimeout(() => {\n window._GM_menuCommands?.find(e => e.name == '开启翻译')?.callback()\n }, 1000)\n }\n}\n\n// export async function loadImtSdk(isAutoLoadImt = false) {\n// if (!isAutoLoadImt && !localStorage.getItem('PXV_IMT_SDK_CFMED')) {\n// const res = await Dialog.confirm({\n// title: '加载沉浸式翻译 SDK',\n// message: '提示:如果已安装沉浸式翻译浏览器扩展则无需加载沉浸式翻译 SDK。可在偏好设置中打开自动加载沉浸式翻译 SDK。',\n// lockScroll: false,\n// closeOnPopstate: true,\n// cancelButtonText: '取消',\n// confirmButtonText: '加载',\n// }).catch(() => 'cancel')\n// if (res != 'confirm') return\n// localStorage.setItem('PXV_IMT_SDK_CFMED', '1')\n// }\n// if (window.immersiveTranslateConfig) return\n// window.immersiveTranslateConfig = {\n// isAutoTranslate: isAutoLoadImt,\n// pageRule: {\n// selectors: [\n// '.novel_text',\n// '.artwork-meta .name-box .title',\n// '.artwork-meta .name-box .series',\n// '.artwork-meta .caption',\n// '.artwork .series-btns',\n// '.users .info .detail .content',\n// '.comments-popup .content',\n// '.live_detail .content .title',\n// '.live_detail .content .chat-item span:last-child',\n// ],\n// translationClasses: ['imt-res-text'],\n// },\n// }\n// await loadScript('https://download.immersivetranslate.com/immersive-translate-sdk-latest.js')\n// const style = document.createElement('style')\n// style.innerHTML = `\n// .imt-fb-more-buttons .btn-animate:first-child,\n// .imt-fb-more-buttons .btn-animate:last-child,\n// .btn-animate[title=\"关闭悬浮球\"],\n// .popup-container .popup-content > div.flex:first-child,\n// .popup-container .trial-pro-container,\n// .popup-container .text-sm.px-1.text-gray-2,\n// .popup-container .widgets-container.mt-5,\n// .popup-container footer,\n// .translation-service-container .custom-select-item:has(.custom-select-item-pro),\n// .translation-service-container select option[value=\"deepl\"],\n// .translation-service-container select option[value=\"openai\"],\n// .translation-service-container select option[value=\"gemini\"],\n// .translation-service-container select option[value=\"claude\"],\n// .translation-service-container select option[value=\"more\"] {\n// display: none !important;\n// }`\n// setTimeout(() => {\n// document.querySelector('#immersive-translate-popup')?.shadowRoot?.appendChild(style)\n// try {\n// const buildinConfig = JSON.parse(localStorage.buildinConfig).buildinConfig\n// buildinConfig.telemetry = false\n// // buildinConfig.translationService = 'bing'\n// buildinConfig.translationService = 'siliconcloud'\n// buildinConfig.translationServices.siliconcloud.APIKEY = SILICON_CLOUD_API_KEY\n// buildinConfig.translationServices.siliconcloud.assistantId = 'common'\n// buildinConfig.translationServices.siliconcloud.model = 'Qwen/Qwen2.5-7B-Instruct'\n// localStorage.buildinConfig = JSON.stringify({ buildinConfig })\n// const userConfig = JSON.parse(localStorage.userConfig).userConfig\n// userConfig.telemetry = false\n// // userConfig.translationService = 'bing'\n// userConfig.translationService = 'siliconcloud'\n// userConfig.translationServices = userConfig.translationServices || {}\n// userConfig.translationServices.siliconcloud = userConfig.translationServices.siliconcloud || {}\n// userConfig.translationServices.siliconcloud.APIKEY = SILICON_CLOUD_API_KEY\n// userConfig.translationServices.siliconcloud.assistantId = 'common'\n// userConfig.translationServices.siliconcloud.model = 'Qwen/Qwen2.5-7B-Instruct'\n// localStorage.userConfig = JSON.stringify({ userConfig })\n// } catch (err) {\n// console.log('err: ', err)\n// }\n// }, 800)\n// }\n\nexport function getNoTranslateWords(tags = []) {\n return new Promise(resolve => {\n Dialog.confirm({\n title: '填写不翻译的文本',\n message: `\n <div id=\"get_pnt_nots_dialog\">\n <p style=\"margin:0.2rem 0\">选择或输入不翻译的单词,以英文逗号分隔,留空跳过</p>\n <input id=\"get_pnt_nots_input\" type=\"text\" >\n <div style=\"height:1px;margin:0.2rem 0;border-bottom:1px solid #ccc\"></div>\n ${tags.map(e => `<div class=\"sel_block_chks\"><input type=\"checkbox\" data-tagname=\"${e.name}\" onchange=\"if(this.checked){window['get_pnt_nots_input'].value=window['get_pnt_nots_input'].value.split(',').filter(e=>e).concat([this.getAttribute('data-tagname')]).join(',')}else{window['get_pnt_nots_input'].value=window['get_pnt_nots_input'].value.split(',').filter(e=>e&&e!=this.getAttribute('data-tagname')).join(',')}\">${e.name}</div>`).join('')}\n </div>`,\n lockScroll: false,\n closeOnPopstate: true,\n cancelButtonText: i18n.t('common.cancel'),\n confirmButtonText: i18n.t('common.confirm'),\n beforeClose: (action, done) => {\n if (action == 'confirm') {\n const tagInp = document.querySelector('#get_pnt_nots_input')?.value || ''\n resolve(tagInp)\n }\n done()\n },\n }).catch(() => {})\n })\n}\n\nexport const aiModelMap = {\n glm: 'THUDM/GLM-4-9B-0414',\n qwen2_5: 'Qwen/Qwen2.5-7B-Instruct',\n hy_mt: 'tencent/Hunyuan-MT-7B',\n qwen3: 'Qwen/Qwen3-8B',\n qwen3_5: 'Qwen/Qwen3.5-4B',\n}\nexport async function siliconCloudTranslate(novelText = '', notsArr = [], aimd = 'glm', onRead = console.log) {\n try {\n if (!novelText.trim()) return\n novelText = replaceNovelMark(novelText)\n if (notsArr.length) {\n notsArr.forEach((e, i) => {\n novelText = novelText.replaceAll(e, `[名字${i}]`)\n })\n }\n const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'authorization': `Bearer ${SILICON_CLOUD_API_KEY}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model: aiModelMap[aimd],\n stream: true,\n messages: [\n {\n role: 'system',\n // content: 'You are a professional, authentic machine translation engine.',\n // content: 'You are a highly skilled translation engine with expertise in eBook translation. Your function is to translate eBook texts accurately into the Simplified Chinese Language, maintaining the original tone, style, and formatting. Focus on delivering translations that resonate with the intended audience while ensuring the essence of the original text is preserved.',\n // content: 'You are a highly skilled translation engine with expertise in fiction literature. Your function is to translate texts into the Simplified Chinese Language, capturing the narrative depth and emotional nuances of the original work. Maintain the original storytelling elements and cultural references without adding any explanations or annotations.',\n // content: '你是一个专业的、正宗的机器翻译引擎。',\n content: 'You are a professional literary translator for Pixiv Japanese novels.',\n },\n {\n role: 'user',\n // content: `Translate the text starting on the next line into Simplified Chinese Language, output translation ONLY. NO explanations. NO notes. The content in \"「」\" also needs to be translated. Input:\\n${novelText}`,\n // content: `Translate the following source text to Simplified Chinese Language, Output translation directly without any additional text.\\nSource Text: ${novelText}`,\n // content: `将下面的文本翻译为简体中文,直接输出翻译结果,不附加其他文本。\\n${novelText}`,\n content: `Translate the following source text into Simplified Chinese Language. Ensure the translation is fluent and natural, maintaining the original meaning and style. Keep names, tone, pacing, and line intent consistent with the source. Do not censor or skip any content. Provide only the translation, without any explanation.\\nSource Text:\\n${novelText}`,\n },\n ],\n }),\n })\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n const chunk = decoder.decode(value, { stream: true })\n const jsonLines = chunk.split('\\n').filter(line => line.trim() !== '')\n\n for (let jsonLine of jsonLines) {\n jsonLine = jsonLine.replace(/^data: /, '')\n if (jsonLine === '[DONE]') {\n onRead({ done: true })\n return\n }\n const json = JSON.parse(jsonLine)\n const { content, reasoning_content: reason } = json.choices[0].delta\n\n if (content) {\n onRead({ done: false, content })\n } else if (reason) {\n onRead({ done: false, content: reason, reasoning: true })\n }\n }\n }\n } catch (err) {\n console.log('siliconCloudTranslate err: ', err)\n }\n}\n\nfunction replaceNovelMark(text) {\n return text.replace(/\\[newpage\\]/g, '\\n\\n————————\\n\\n')\n .replace(/\\[\\[rb:([^>[\\]]+) *> *([^>[\\]]+)\\]\\]/g, '$1($2)')\n .replace(/\\[\\[jumpuri:([^>\\s[\\]]+) *> *([^>\\s[\\]]+)\\]\\]/g, '$1')\n .replace(/\\[\\[emphasismark:([^>[\\]]+) *> *([^>[\\]]+)\\]\\]/g, '$1')\n .replace(/\\[i:([^[\\]]+)\\]/g, '$1')\n .replace(/\\[b:([^[\\]]+)\\]/g, '$1')\n .replace(/\\[pixivimage:([\\d-]+)\\]/g, 'https://pximg.cocomi.eu.org/-pid-/$1')\n .replace(/\\[chapter: *([^[\\]]+)\\]/g, '\\n$1\\n')\n .replace(/\\[uploadedimage:(\\d+)\\]/g, '')\n}\n\nexport const isNativeTranslatorSupported = 'Translator' in self && !navigator.userAgent.includes('Edg/')\nlet translator\nasync function ensureTranslator() {\n if (translator) return\n translator = await window.Translator.create({\n sourceLanguage: 'ja',\n targetLanguage: 'zh',\n })\n}\nexport async function nativeTranslate(novelText = '', onRead = () => {}) {\n try {\n if (!novelText.trim()) return\n novelText = replaceNovelMark(novelText)\n const textArr = novelText.split('\\n')\n await ensureTranslator()\n for (const text of textArr) {\n if (!text.trim()) continue\n const stream = translator.translateStreaming(text)\n for await (const chunk of stream) {\n console.log(chunk)\n onRead({ done: false, content: chunk })\n }\n onRead({ done: false, content: '\\n' })\n }\n onRead({ done: true })\n } catch (err) {\n console.log('nativeTranslate err: ', err)\n }\n}\n"}}, "src/utils/translate/manga.js": {"2f6c21c9382c42fd83c7a2aa0932a38a7acfcdc4": {"t": 1785674139.935781, "n": 4560, "h": "61cf1b9ff6b18519", "c": "import { SILICON_CLOUD_API_KEY } from '@/consts'\nimport { getCache, setCache } from '@/utils/storage/siteCache'\n\n/**\n * Get cached translation for a manga page\n * @param {string|number} artworkId\n * @param {number} pageIndex\n * @returns {Promise<string|null>}\n */\nexport async function getCachedTranslation(artworkId, pageIndex) {\n const key = `pic.translate.${artworkId}.${pageIndex}`\n const cached = await getCache(key, null)\n return cached\n}\n\n/**\n * Call SiliconCloud multimodal API with SSE streaming\n * @param {string} imageSrc\n * @param {function} onRead - callback({ content: string, done: boolean })\n */\nexport async function callMultimodalAPIStream(imageSrc, onRead) {\n const url = 'https://api.siliconflow.cn/v1/chat/completions'\n const imageUrl = new URL(imageSrc)\n imageUrl.protocol = 'https:'\n imageUrl.hostname = 'img.rika.club'\n imageUrl.port = ''\n const body = {\n enable_thinking: false,\n stream: true,\n model: 'nex-agi/Nex-N2-Pro',\n // model: 'moonshotai/Kimi-K2.7-Code',\n // model: 'Pro/moonshotai/Kimi-K2.6',\n // model: 'zai-org/GLM-4.5V',\n // model: 'Qwen/Qwen3.6-35B-A3B',\n // model: 'Qwen/Qwen3.6-27B',\n // model: 'Qwen/Qwen3.5-122B-A10B',\n // model: 'Qwen/Qwen3.5-35B-A3B',\n // model: 'Qwen/Qwen3.5-27B',\n // model: 'Qwen/Qwen3-VL-32B-Instruct',\n // model: 'Qwen/Qwen3-VL-8B-Instruct',\n // model: 'Qwen/Qwen3-VL-30B-A3B-Instruct',\n // model: 'Qwen/Qwen3.5-9B',\n // model: 'Qwen/Qwen3.5-4B',\n messages: [{\n role: 'user',\n content: [\n { type: 'image_url', image_url: { url: imageUrl } },\n { type: 'text', text: '请把这张漫画翻译为中文,按照从右到左、从上到下的顺序输出文本位置(比如第几格左或右)、文本原文、文本翻译后的中文,不附加任何解释。' },\n ],\n }],\n }\n\n let response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'authorization': `Bearer ${SILICON_CLOUD_API_KEY}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n } catch (err) {\n throw new Error(`Network error calling SiliconCloud API: ${err.message}`)\n }\n\n if (!response.ok) {\n throw new Error(`SiliconCloud API returned status ${response.status} ${response.statusText}`)\n }\n\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n\n const chunk = decoder.decode(value, { stream: true })\n const jsonLines = chunk.split('\\n').filter(line => line.trim() !== '')\n\n for (let jsonLine of jsonLines) {\n jsonLine = jsonLine.replace(/^data: /, '')\n if (jsonLine === '[DONE]') {\n onRead({ content: '', done: true })\n return\n }\n\n let content = ''\n try {\n const json = JSON.parse(jsonLine)\n content = json.choices[0].delta.content || ''\n } catch (e) {\n continue\n }\n\n if (content) {\n content = content.replace(/<\\|begin_of_box\\||<\\|end_of_box\\|>|^>+\\s*/g, '')\n if (content) {\n onRead({ content, done: false })\n }\n }\n }\n }\n\n onRead({ content: '', done: true })\n}\n\n/**\n * Translate a manga page image — checks cache first, streams API if needed\n * @param {string} imageUrl\n * @param {string|number} artworkId\n * @param {number} pageIndex\n * @param {function} onRead - callback({ content: string, done: boolean, error?: string })\n * @returns {Promise<string|null>}\n */\nexport async function translateMangaPage(imageUrl, artworkId, pageIndex, onRead) {\n const key = `pic.translate.${artworkId}.${pageIndex}`\n\n const cached = await getCachedTranslation(artworkId, pageIndex)\n if (cached) {\n onRead({ content: cached, done: true })\n return cached\n }\n\n // Non-streaming: collect full text, callback once\n if (!onRead) {\n let fullText = ''\n try {\n await callMultimodalAPIStream(imageUrl, ({ content, done }) => {\n fullText += content\n })\n } catch (err) {\n console.warn('picTranslate: API call failed', err.message)\n return null\n }\n if (fullText.trim()) {\n await setCache(key, fullText)\n return fullText\n }\n return null\n }\n\n // Streaming mode\n let fullText = ''\n try {\n await callMultimodalAPIStream(imageUrl, ({ content, done }) => {\n fullText += content\n onRead({ content, done })\n })\n } catch (err) {\n console.warn('picTranslate: API call failed', err.message)\n onRead({ content: '', done: true, error: err.message })\n return null\n }\n\n if (fullText.trim()) {\n await setCache(key, fullText)\n return fullText\n }\n\n return null\n}\n"}}}}