More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. 未分类
  3. 正文

DeepWiki翻译

2025年8月31日 25点热度 0人点赞 0条评论

deepwiki输出的质量比较好,但是英文的读起来没那么顺畅,GPT帮忙写了个翻译的脚本

我个人用的是gemini2.5 flash,可以自行配置。把代码粘贴到油猴中就可以了

// ==UserScript==
// @name         DeepWiki Instant Translator (EN↔ZH)
// @namespace    https://chat.openai.com/
// @version      0.1.0
// @description  Translate DeepWiki pages on the fly with OpenAI or DeepL; preserves code blocks; works with dynamically loaded content.
// @author       You
// @match        https://deepwiki.com/*
// @match        https://www.deepwiki.com/*
// @run-at       document-end
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.openai.com
// @connect      api.deepl.com
// @connect      api-free.deepl.com
// @connect      openrouter.ai
// @connect      *.openrouter.ai   // (可选)如果你用的是区域/镜像域名
// @connect      api.openrouter.ai
// ==/UserScript==

(function () {
  'use strict';

  // -------------------- Config (editable in UI) --------------------
  const DEFAULTS = {
    provider: 'openai', // 'openai' | 'deepl'
    openaiModel: 'google/gemini-2.5-flash',
    openaiBase: 'https://api.openrouter.ai/v1/',
    deeplBase: 'https://api-free.deepl.com/v2/translate', // change to https://api.deepl.com/v2/translate if you have Pro
    apiKey: '',
    direction: 'auto', // 'auto' | 'en2zh' | 'zh2en'
    translateScope: 'main', // 'main' | 'body'
    maxCharsPerBatch: 1800,
    concurrency: 1,
    cacheTTLHours: 72
  };

  // Keys for GM storage
  const KV = {
    CFG: 'dw_i18n_cfg_v1',
    CACHE: 'dw_i18n_cache_v1'
  };

  // -------------------- Utilities --------------------
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  function loadCfg() {
    const raw = GM_getValue(KV.CFG);
    if (!raw) return { ...DEFAULTS };
    try { return { ...DEFAULTS, ...JSON.parse(raw) }; } catch (e) { return { ...DEFAULTS }; }
  }

  function saveCfg(cfg) {
    GM_setValue(KV.CFG, JSON.stringify(cfg));
  }

  function loadCache() {
    const raw = GM_getValue(KV.CACHE);
    if (!raw) return {};
    try { return JSON.parse(raw); } catch (e) { return {}; }
  }

  function saveCache(cache) {
    GM_setValue(KV.CACHE, JSON.stringify(cache));
  }

  function clearCache() {
    CACHE = {};
    saveCache(CACHE);
  }

  function hashKey(s) {
    // simple djb2 hash -> string
    let h = 5381;
    for (let i = 0; i < s.length; i++) h = ((h << 5) + h) + s.charCodeAt(i);
    return 'h' + (h >>> 0);
  }

  function isCJK(str) {
    return /[\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff]/.test(str);
  }

  function detectDirection(textSample, cfg) {
    if (cfg.direction !== 'auto') return cfg.direction;
    // Heuristic: if mostly CJK, translate to English; else to Chinese
    return isCJK(textSample) ? 'zh2en' : 'en2zh';
  }

  function isVisible(el) {
    const style = el.nodeType === Node.ELEMENT_NODE ? getComputedStyle(el) : getComputedStyle(el.parentElement);
    return style && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
  }

  function isInside(node, tagsUpper) {
    let p = node.parentNode;
    while (p) {
      if (p.nodeType === Node.ELEMENT_NODE) {
        const tag = p.tagName.toUpperCase();
        if (tagsUpper.includes(tag)) return true;
        if (p.getAttribute && p.getAttribute('data-i18n-skip') === '1') return true;
      }
      p = p.parentNode;
    }
    return false;
  }

  function selectRoot(cfg) {
    const main = document.querySelector('main');
    if (cfg.translateScope === 'main' && main) return main;
    return document.body;
  }

  function makeWalker(root) {
    return document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        if (!node || !node.nodeValue) return NodeFilter.FILTER_REJECT;
        const text = node.nodeValue.replace(/[\s\u00A0]+/g, ' ').trim();
        if (!text) return NodeFilter.FILTER_REJECT;
        if (text.length < 2) return NodeFilter.FILTER_REJECT;
        if (isInside(node, ['CODE', 'PRE', 'KBD', 'SAMP', 'SCRIPT', 'STYLE', 'NOSCRIPT', 'MATH', 'SVG', 'TEXTAREA', 'INPUT'])) return NodeFilter.FILTER_REJECT;
        if (!isVisible(node)) return NodeFilter.FILTER_REJECT;
        return NodeFilter.FILTER_ACCEPT;
      }
    });
  }

  function splitForBatches(items, maxChars) {
    const batches = [];
    let cur = [];
    let total = 0;
    for (const it of items) {
      const len = it.text.length;
      if (len > maxChars) {
        // split long text by sentences or punctuation
        const chunks = it.text.split(/(?<=[.!?。!?;;])\s+/);
        for (const ch of chunks) {
          if (!ch.trim()) continue;
          batches.push([{ ...it, text: ch }]);
        }
        continue;
      }
      if (total + len > maxChars && cur.length) {
        batches.push(cur);
        cur = [];
        total = 0;
      }
      cur.push(it);
      total += len + 1;
    }
    if (cur.length) batches.push(cur);
    return batches;
  }

  function wrapNode(node) {
    // Store original text and put a span wrapper to allow toggling
    const span = document.createElement('span');
    span.className = 'dw-i18n-span';
    span.dataset.original = node.nodeValue;
    node.parentNode.replaceChild(span, node);
    span.textContent = node.nodeValue;
    return span;
  }

  function collectTextNodes(root) {
    const walker = makeWalker(root);
    const items = [];
    let node;
    while ((node = walker.nextNode())) {
      if (!node.nodeValue || !node.parentNode) continue;
      items.push({ node, text: node.nodeValue });
    }
    return items;
  }

  function nowTs() { return Date.now(); }
  function hours(ms) { return ms / 3600000; }

  function getCached(cache, text) {
    const key = hashKey(text);
    const rec = cache[key];
    if (!rec) return null;
    if (hours(nowTs() - rec.ts) > CFG.cacheTTLHours) return null;
    return rec.val;
  }

  function setCached(cache, text, val) {
    const key = hashKey(text);
    cache[key] = { val, ts: nowTs() };
  }

  // Normalize OpenAI base to ensure /chat/completions is present
  function normalizeOpenAIBase(url) {
    if (!url) return '';
    try {
      const u = new URL(url);
      // If base ends with /v1 or /v1/, append chat/completions
      if (/\/v1\/?/.test(u.pathname)) {
        u.pathname = u.pathname.replace(/\/v1\/?/, '/v1/chat/completions');
      }
      // If path is /api/v1 or /api/v1/, also append
      if (/\/api\/v1\/?/.test(u.pathname)) {
        u.pathname = u.pathname.replace(/\/api\/v1\/?/, '/api/v1/chat/completions');
      }
      return u.toString();
    } catch (_) {
      return url;
    }
  }

  // -------------------- Providers --------------------
  async function translateOpenAI(batchText, direction, cfg) {
    const sys = direction === 'en2zh'
      ? 'You are a professional English→Chinese translator. Translate the user text to natural, concise Simplified Chinese. Preserve code, math, and inline code exactly as-is. Keep line breaks. Return ONLY the translated text.'
      : 'You are a professional Chinese→English translator. Translate the user text to clear, idiomatic English for technical documentation. Preserve code, math, and inline code exactly as-is. Keep line breaks. Return ONLY the translated text.';

    const base = normalizeOpenAIBase(cfg.openaiBase);
    let res = await gmFetch(base, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': `Bearer {cfg.apiKey}`,
        'X-Title': 'DeepWiki Instant Translator'
      },
      data: JSON.stringify({
        model: cfg.openaiModel,
        temperature: 0.2,
        messages: [
          { role: 'system', content: sys },
          { role: 'user', content: batchText }
        ]
      }),
      noFetchFallback: true
    });

    if (!res.ok) {
      // 若是 0(网络/DNS),尝试在 api.openrouter.ai 与 openrouter.ai/api 间切换
      if (res.status === 0 && typeof base === 'string' && /openrouter\.ai/.test(base)) {
        const alt = base.includes('api.openrouter.ai')
          ? base.replace('api.openrouter.ai', 'openrouter.ai/api')
          : base.replace('openrouter.ai/api', 'api.openrouter.ai');
        const retry0 = await gmFetch(alt, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': `Bearer{cfg.apiKey}`,
            'X-Title': 'DeepWiki Instant Translator'
          },
          data: JSON.stringify({
            model: cfg.openaiModel,
            temperature: 0.2,
            messages: [
              { role: 'system', content: sys },
              { role: 'user', content: batchText }
            ]
          }),
          noFetchFallback: true
        });
        if (retry0.ok) {
          res = retry0;
        } else {
          throw new Error(`OpenAI HTTP {retry0.status}; finalUrl:{retry0.finalUrl || ''}`);
        }
      } else {
        throw new Error(`OpenAI HTTP {res.status}; finalUrl:{res.finalUrl || ''}`);
      }
    }
    let ct = (res.headers || '').toLowerCase();
    if (ct && !ct.includes('application/json')) {
      // 如果命中 openrouter 网页而非 API,尝试回退到 api.openrouter.ai
      if (typeof cfg.openaiBase === 'string' && cfg.openaiBase.includes('openrouter.ai/api/')) {
        const alt = cfg.openaiBase.replace('openrouter.ai/api/', 'api.openrouter.ai/');
        const retry = await gmFetch(alt, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': `Bearer {cfg.apiKey}`,
            'X-Title': 'DeepWiki Instant Translator'
          },
          data: JSON.stringify({
            model: cfg.openaiModel,
            temperature: 0.2,
            messages: [
              { role: 'system', content: sys },
              { role: 'user', content: batchText }
            ]
          })
        });
        if (retry.ok) {
          res = retry;
          ct = (res.headers || '').toLowerCase();
        }
      }
    }
    if (ct && !ct.includes('application/json')) {
      const preview = (res.responseText || '').slice(0, 200);
      throw new Error(`OpenAI: unexpected content-type:{ct}; finalUrl: {res.finalUrl || ''}; preview:{preview}`);
    }
    let json;
    try {
      json = JSON.parse(res.responseText);
    } catch (err) {
      const preview = (res.responseText || '').slice(0, 200);
      throw new Error(`OpenAI: invalid JSON: {err && err.message ? err.message : String(err)}; finalUrl:{res.finalUrl || ''}; preview: {preview}`);
    }
    const text = json.choices?.[0]?.message?.content?.trim();
    if (!text) throw new Error('OpenAI: empty response');
    return text;
  }

  async function translateDeepL(batchText, direction, cfg) {
    const target_lang = direction === 'en2zh' ? 'ZH' : 'EN-US';
    const source_lang = direction === 'en2zh' ? 'EN' : 'ZH';

    const res = await gmFetch(cfg.deeplBase, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': `DeepL-Auth-Key{cfg.apiKey}` },
      data: new URLSearchParams({ text: batchText, target_lang, source_lang }).toString()
    });
    if (!res.ok) throw new Error(`DeepL HTTP {res.status}`);
    const ct = (res.headers || '').toLowerCase();
    if (ct && !ct.includes('application/json')) {
      const preview = (res.responseText || '').slice(0, 200);
      throw new Error(`DeepL: unexpected content-type:{ct}; preview: {preview}`);
    }
    let json;
    try {
      json = JSON.parse(res.responseText);
    } catch (err) {
      const preview = (res.responseText || '').slice(0, 200);
      throw new Error(`DeepL: invalid JSON:{err && err.message ? err.message : String(err)}; preview: {preview}`);
    }
    const text = json.translations?.[0]?.text?.trim();
    if (!text) throw new Error('DeepL: empty response');
    return text;
  }

  async function gmFetch(url, opts) {
    const noFetchFallback = !!opts.noFetchFallback;
    const gmResult = await new Promise((resolve) => {
      GM_xmlhttpRequest({
        url,
        method: opts.method || 'GET',
        headers: opts.headers || {},
        data: opts.data,
        responseType: 'text',
        timeout: 30000,
        onload: (r) => resolve({ ok: r.status >= 200 && r.status<300, status: r.status, responseText: r.responseText, headers: r.responseHeaders || '', finalUrl: r.finalUrl || '' }),
        onerror: (e) => resolve({ ok: false, status: 0, responseText: String(e && e.error ? e.error : e), headers: '', finalUrl: '' }),
        ontimeout: () => resolve({ ok: false, status: 0, responseText: 'timeout', headers: '', finalUrl: '' }),
        onabort: () => resolve({ ok: false, status: 0, responseText: 'aborted', headers: '', finalUrl: '' })
      });
    });
    if (gmResult.ok || gmResult.status !== 0 || noFetchFallback) return gmResult;

    // 回退到原生 fetch(若目标支持 CORS)
    try {
      const resp = await fetch(url, {
        method: opts.method || 'GET',
        headers: opts.headers || {},
        body: opts.data,
        mode: 'cors',
        credentials: 'omit',
        cache: 'no-cache'
      });
      const text = await resp.text();
      let headerStr = '';
      try {
        resp.headers.forEach((v, k) => { headerStr += `{k}:${v}\n`; });
      } catch (_) { /* ignore */ }
      return { ok: resp.ok, status: resp.status, responseText: text, headers: headerStr, finalUrl: resp.url || '' };
    } catch (err) {
      return { ok: false, status: 0, responseText: String(err && err.message ? err.message : err), headers: '', finalUrl: '' };
    }
  }

  // -------------------- Core translate pipeline --------------------
  let CFG = loadCfg();
  // 迁移:修正旧配置中的 openrouter 网页端点为 API 端点
  if (CFG.openaiBase && CFG.openaiBase.includes('openrouter.ai/api/')) {
    CFG.openaiBase = CFG.openaiBase.replace('openrouter.ai/api/', 'api.openrouter.ai/');
    saveCfg(CFG);
  }
  let CACHE = loadCache();

  const SEP = '\n<<<SEG>>>\n';

  async function translateNodes(nodes, dir) {
    // 1) Wrap nodes with spans if not already
    const spans = nodes.map(({ node, text }) => {
      if (node.parentNode && node.parentNode.classList && node.parentNode.classList.contains('dw-i18n-span')) {
        return { span: node.parentNode, text };
      }
      const span = wrapNode(node);
      return { span, text };
    });

    // 2) Prepare work items (skip cached)
    const work = [];
    for (const it of spans) {
      const cached = getCached(CACHE, it.text);
      if (cached) {
        it.span.dataset.translated = cached;
        continue;
      }
      work.push({ span: it.span, text: it.text });
    }

    if (!work.length) {
      // just apply translated text from cache
      for (const it of spans) if (it.span.dataset.translated) it.span.textContent = it.span.dataset.translated;
      return;
    }

    const batches = splitForBatches(work, CFG.maxCharsPerBatch);

    for (const batch of batches) {
      const joined = batch.map((b) => b.text).join(SEP);
      const provider = CFG.provider;
      let translatedJoined = '';
      try {
        if (provider === 'openai') translatedJoined = await translateOpenAI(joined, dir, CFG);
        else translatedJoined = await translateDeepL(joined, dir, CFG);
      } catch (e) {
        console.error('[DW-I18N] translate error', e);
        continue;
      }

      const parts = translatedJoined.split(SEP);
      if (parts.length !== batch.length) {
        // fallback: naive split by line count
        console.warn('[DW-I18N] Segment mismatch; applying naive mapping');
        for (let i = 0; i < batch.length; i++) {
          const piece = parts[i] || translatedJoined;
          batch[i].span.dataset.translated = piece;
          batch[i].span.textContent = piece;
          setCached(CACHE, batch[i].text, piece);
        }
      } else {
        for (let i = 0; i < batch.length; i++) {
          const piece = parts[i];
          batch[i].span.dataset.translated = piece;
          batch[i].span.textContent = piece;
          setCached(CACHE, batch[i].text, piece);
        }
      }
      saveCache(CACHE);
      await sleep(50);
    }
  }

  // Debounce util
  function debounce(fn, wait) {
    let t; return function (...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), wait); };
  }

  // -------------------- UI --------------------
  function createPanel() {
    const host = document.createElement('div');
    host.id = 'dw-i18n-panel-host';
    document.documentElement.appendChild(host);
    const root = host.attachShadow({ mode: 'open' });

    const html = `
      <style>
        .card{ position:fixed; right:16px; bottom:16px; z-index:2147483647; background:#111827; color:#e5e7eb; padding:12px; border-radius:14px; box-shadow:0 10px 30px rgba(0,0,0,.4); font: 13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial; width: 320px }
        .row{ display:flex; gap:8px; align-items:center; margin:6px 0 }
        .row label{ flex:0 0 92px; color:#9ca3af }
        .row input[type="text"], .row input[type="password"], .row select { flex:1; background:#0b1220; border:1px solid #374151; border-radius:8px; padding:6px 8px; color:#e5e7eb }
        .row input::placeholder { color:#6b7280 }
        .actions{ display:flex; gap:8px; margin-top:10px }
        .btn{ flex:1; background:#2563eb; color:white; border:none; border-radius:10px; padding:8px 10px; cursor:pointer }
        .btn.secondary{ background:#374151 }
        .small{ font-size:12px; color:#9ca3af }
        .pill{ display:inline-flex; gap:6px; align-items:center; background:#0b1220; border:1px solid #374151; border-radius:999px; padding:4px 8px }
        .toggle{ cursor:pointer; }
      </style>
      <div class="card">
        <div class="row"><strong>DeepWiki 即时翻译</strong><span class="pill" id="status">待机</span></div>
        <div class="row"><label>服务商</label>
          <select id="provider">
            <option value="openai">OpenAI</option>
            <option value="deepl">DeepL</option>
          </select>
        </div>
        <div class="row" id="modelRow"><label>模型</label><input id="model" type="text" placeholder="gpt-4o-mini"/></div>
        <div class="row"><label>API Key</label><input id="apikey" type="password" placeholder="粘贴你的密钥"/></div>
        <div class="row" id="baseRow"><label>接口地址</label><input id="base" type="text" placeholder="https://api.openrouter.ai/v1/chat/completions"/></div>
        <div class="row"><label>方向</label>
          <select id="direction">
            <option value="auto">自动检测</option>
            <option value="en2zh">英 → 中</option>
            <option value="zh2en">中 → 英</option>
          </select>
        </div>
        <div class="row"><label>范围</label>
          <select id="scope">
            <option value="main">仅 main</option>
            <option value="body">整个页面</option>
          </select>
        </div>
        <div class="actions">
          <button class="btn" id="doTrans">翻译</button>
          <button class="btn secondary" id="toggle">原文/译文</button>
          <button class="btn secondary" id="clearCache">清理缓存</button>
        </div>
        <div class="small">提示:代码块、内联代码、MathJax 将被自动跳过;内容变化时自动监听。</div>
      </div>
    `;

    root.innerHTML = html;

    const qs = (sel) => root.querySelector(sel);

    const ui = {
      host, root,
      status: qs('#status'),
      provider: qs('#provider'),
      model: qs('#model'),
      apikey: qs('#apikey'),
      base: qs('#base'),
      direction: qs('#direction'),
      scope: qs('#scope'),
      // auto removed
      doTrans: qs('#doTrans'),
      toggle: qs('#toggle')
      ,clearCache: qs('#clearCache')
    };

    // init from CFG
    ui.provider.value = CFG.provider;
    ui.model.value = CFG.openaiModel;
    ui.apikey.value = CFG.apiKey;
    ui.base.value = CFG.openaiBase;
    ui.direction.value = CFG.direction;
    ui.scope.value = CFG.translateScope;
    ui.model.parentElement.style.display = CFG.provider === 'openai' ? '' : 'none';
    ui.base.parentElement.style.display = CFG.provider === 'openai' ? '' : 'none';

    // events
    ui.provider.onchange = () => {
      CFG.provider = ui.provider.value; saveCfg(CFG);
      ui.model.parentElement.style.display = CFG.provider === 'openai' ? '' : 'none';
      ui.base.parentElement.style.display = CFG.provider === 'openai' ? '' : 'none';
    };
    ui.model.oninput = () => { CFG.openaiModel = ui.model.value.trim(); saveCfg(CFG); };
    ui.apikey.oninput = () => { CFG.apiKey = ui.apikey.value.trim(); saveCfg(CFG); };
    ui.base.oninput = () => { CFG.openaiBase = normalizeOpenAIBase(ui.base.value.trim()); ui.base.value = CFG.openaiBase; saveCfg(CFG); };
    ui.direction.onchange = () => { CFG.direction = ui.direction.value; saveCfg(CFG); };
    ui.scope.onchange = () => { CFG.translateScope = ui.scope.value; saveCfg(CFG); };

    ui.doTrans.onclick = runTranslate;
    ui.toggle.onclick = toggleText;
    ui.clearCache.onclick = clearCacheAndReset;

    return ui;
  }

  function setStatus(ui, text) { ui.status.textContent = text; }

  let showingTranslation = true;
  function toggleText() {
    const spans = document.querySelectorAll('.dw-i18n-span');
    for (const s of spans) {
      const orig = s.dataset.original || '';
      const tr = s.dataset.translated || '';
      if (showingTranslation) s.textContent = orig; else if (tr) s.textContent = tr;
    }
    showingTranslation = !showingTranslation;
  }

  function clearCacheAndReset() {
    clearCache();
    // 将页面切回原文
    const spans = document.querySelectorAll('.dw-i18n-span');
    for (const s of spans) {
      const orig = s.dataset.original || '';
      s.textContent = orig;
      delete s.dataset.translated;
    }
    setStatus(UI, '缓存已清空');
  }

  let isTranslating = false;
  const runTranslate = debounce(async function () {
    if (isTranslating) return;
    isTranslating = true;
    if (!CFG.apiKey) { alert('请先在面板中填写 API Key'); return; }
    const root = selectRoot(CFG);
    const nodes = collectTextNodes(root);
    if (!nodes.length) return;

    const sample = nodes.slice(0, 20).map(n => n.text).join(' ');
    const dir = detectDirection(sample, CFG);

    setStatus(UI, dir === 'en2zh' ? '英→中翻译中…' : '中→英翻译中…');
    try {
      await translateNodes(nodes, dir);
    } finally {
      isTranslating = false;
    }
    setStatus(UI, '完成');
  }, 250);

  // -------------------- Observe DOM changes --------------------
  let UI = null;
  function startObserver() {
    const root = selectRoot(CFG);
    const obs = new MutationObserver(debounce((muts) => {
      // 自动翻译已移除,这里不再触发自动翻译
    }, 400));
    obs.observe(root, { childList: true, subtree: true, characterData: true });
  }

  // -------------------- Boot --------------------
  function boot() {
    UI = createPanel();
    startObserver();
    // 自动翻译已移除,启动时不自动翻译
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  else boot();
})();

标签: 暂无
最后更新:2025年8月31日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS