Ask Deepseek

Registers "Ask Deepseek" in the native browser context menu (via Tampermonkey) and adds the Ctrl+Y keyboard shortcut: saves the selected text, opens chat.deepseek.com in a new tab and automatically sends the question with web search enabled.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Ask Deepseek
// @namespace    https://chat.deepseek.com/
// @version      1.3.3
// @description  Registers "Ask Deepseek" in the native browser context menu (via Tampermonkey) and adds the Ctrl+Y keyboard shortcut: saves the selected text, opens chat.deepseek.com in a new tab and automatically sends the question with web search enabled.
// @match        http://*/*
// @match        https://*/*
// @match        https://chat.deepseek.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const DEBUG = true;
  const STORAGE_KEY = 'pendingQuery';
  const DEEPSEEK_HOST = 'chat.deepseek.com';
  const UI_TIMEOUT_MS = 15000;

  const log = (...a) => console.log('[Ask Deepseek]', ...a);
  const warn = (...a) => console.warn('[Ask Deepseek]', ...a);

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

  /* ------------------------------------------------------------------ */
  /*  DeepSeek UI selectors (multiple fallbacks)                        */
  /* ------------------------------------------------------------------ */
  const INPUT_SELECTORS = [
    'textarea#chat-input',
    'textarea[data-testid="chat-input"]',
    'textarea[data-testid="chat_input"]',
    'div[contenteditable="true"][role="textbox"]',
    'textarea[placeholder*="Message" i]',
    'div[contenteditable="true"]',
    'textarea[placeholder]',
  ];

  const SEND_SELECTORS = [
    'div[role="button"][aria-label="Send"]',
    'button[aria-label="Send"]',
    'div[aria-label="Send"]',
    'button[data-testid="send-button"]',
    'div[role="button"][class*="send" i]',
  ];

  const WEBSEARCH_TOGGLE_SELECTORS = [
    'button[aria-label*="Web search" i]',
    '[aria-label*="Web search" i]',
    '[title*="Web search" i]',
    '[data-testid*="web-search" i]',
    '[data-testid*="web_search" i]',
    '[data-testid*="websearch" i]',
    'button[class*="web-search" i]',
    'button[class*="web_search" i]',
    '[class*="search-toggle" i]',
  ];

  const MODE_DROPDOWN_SELECTORS = [
    'button[aria-label*="search mode" i]',
    '[data-testid*="search-mode" i]',
    '[data-testid*="search-mode-button" i]',
    'button[class*="search-mode" i]',
  ];

  const MODE_ITEM_TEXT = ['Web search'];
  const MODE_ITEM_SELECTORS = [
    '[role="menuitem"]',
    '[role="option"]',
    'div[class*="menu" i] div[class*="item" i]',
    'li[class*="item" i]',
  ];

  function queryFirst(selectors) {
    for (const sel of selectors) {
      try {
        const el = document.querySelector(sel);
        if (el) return { el, selector: sel };
      } catch (err) {
        warn('Invalid selector:', sel, err);
      }
    }
    return null;
  }

  /* ------------------------------------------------------------------ */
  /*  PART A - All pages: native context menu entry                     */
  /*  The entry is registered natively via GM_registerMenuCommand:      */
  /*  Tampermonkey (4.14+) adds it to the browser's native context      */
  /*  menu. On Firefox, an element injected into the DOM cannot         */
  /*  overlay the native menu, so this is the only robust way.          */
  /*  The entry is always present in the menu: if there is no           */
  /*  selection, clicking it does nothing.                              */
  /* ------------------------------------------------------------------ */
  const SELECTION_FRESHNESS_MS = 10000;
  let lastSelection = null;

  /* Keyboard shortcut (works when the page has focus):
     Ctrl+Y. To change it, modify the values below. */
  const SHORTCUT_KEY = 'KeyY';
  const SHORTCUT_CTRL = true;
  const SHORTCUT_SHIFT = false;

  function partA() {
    document.addEventListener('contextmenu', onContextMenu, true);
    document.addEventListener('keydown', onKeyDown, true);
    if (typeof GM_registerMenuCommand === 'function') {
      GM_registerMenuCommand('\u{1F50D} Ask Deepseek', onMenuClick);
      log('Native context menu entry registered');
    } else {
      warn('GM_registerMenuCommand not available: update Tampermonkey (4.14+)');
    }
  }

  function onKeyDown(e) {
    const modsOk = (SHORTCUT_CTRL === !!e.ctrlKey) && (SHORTCUT_SHIFT === !!e.shiftKey) && !e.altKey && !e.metaKey;
    if (!modsOk || e.code !== SHORTCUT_KEY) return;
    e.preventDefault();
    e.stopPropagation();
    const raw = window.getSelection ? window.getSelection().toString() : '';
    const clean = raw.replace(/\s+/g, ' ').trim();
    if (!clean) {
      log('Shortcut pressed with no selection: no action');
      return;
    }
    log('Shortcut: selection detected (' + clean.length + ' chars)');
    openDeepSeek(clean);
  }

  function onContextMenu() {
    lastSelection = {
      text: window.getSelection ? window.getSelection().toString() : '',
      time: Date.now(),
    };
  }

  function onMenuClick() {
    let raw = '';
    if (lastSelection && Date.now() - lastSelection.time < SELECTION_FRESHNESS_MS) {
      raw = lastSelection.text;
    } else if (window.getSelection) {
      raw = window.getSelection().toString();
    }
    const clean = raw.replace(/\s+/g, ' ').trim();
    if (!clean) {
      log('No selection: no action');
      return;
    }
    log('Selection detected (' + clean.length + ' chars)');
    openDeepSeek(clean);
  }

  function openDeepSeek(text) {
    GM_setValue(STORAGE_KEY, text);
    log('Question saved (' + text.length + ' chars)');
    window.open('https://chat.deepseek.com/', '_blank', 'noopener');
  }

  /* ------------------------------------------------------------------ */
  /*  PART B - chat.deepseek.com: automatic submission                  */
  /* ------------------------------------------------------------------ */
  function partB() {
    const query = GM_getValue(STORAGE_KEY, '');
    GM_deleteValue(STORAGE_KEY);
    if (!query || !query.trim()) {
      log('No pending question');
      return;
    }
    log('Pending question found, waiting for the chat UI to load...');

    waitForChatUI(UI_TIMEOUT_MS).then((ready) => {
      if (!ready) return;
      if (!setInputText(query)) return;
      sleep(500)
        .then(enableWebSearch)
        .then(() => sleep(500))
        .then(sendMessage);
    });
  }

  function waitForChatUI(timeoutMs) {
    return new Promise((resolve) => {
      const started = Date.now();
      const check = () => {
        if (queryFirst(INPUT_SELECTORS)) {
          log('Chat UI ready');
          return resolve(true);
        }
        if (Date.now() - started > timeoutMs) {
          warn('Timeout: DeepSeek UI not ready within ' + timeoutMs + ' ms');
          return resolve(false);
        }
        setTimeout(check, 500);
      };
      check();
    });
  }

  function setInputText(text) {
    const found = queryFirst(INPUT_SELECTORS);
    if (!found) {
      warn('Message input not found, no submission');
      return false;
    }
    const el = found.el;
    el.focus();

    if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
      const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
      const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
      setter.call(el, text);
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
      log('Text set (' + text.length + ' chars) via:', found.selector);
    } else if (el.isContentEditable) {
      try {
        document.execCommand('selectAll', false, null);
        document.execCommand('insertText', false, text);
      } catch (err) {
        el.textContent = text;
        el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
      }
      log('Text set (' + text.length + ' chars) via:', found.selector);
    } else {
      warn('Unsupported input type:', el.tagName);
      return false;
    }
    return true;
  }

  function isToggleActive(el) {
    const attr = el.getAttribute('aria-pressed');
    if (attr !== null) return attr === 'true';
    return /(^|\s)(active|on)(\s|$)/i.test(el.className || '') || (el.getAttribute('data-state') === 'on');
  }

  async function enableWebSearch() {
    const toggle = queryFirst(WEBSEARCH_TOGGLE_SELECTORS);
    if (toggle) {
      if (isToggleActive(toggle.el)) {
        log('Web search already active');
        return;
      }
      toggle.el.click();
      await sleep(300);
      if (isToggleActive(toggle.el)) {
        log('Web search enabled (toggle):', toggle.selector);
        return;
      }
      warn('Toggle clicked but not active:', toggle.selector);
    }

    const dropdown = queryFirst(MODE_DROPDOWN_SELECTORS);
    if (dropdown) {
      dropdown.el.click();
      await sleep(400);
      const items = [];
      for (const sel of MODE_ITEM_SELECTORS) {
        try {
          items.push(...Array.from(document.querySelectorAll(sel)));
        } catch (err) {}
      }
      const target = items.find((i) => MODE_ITEM_TEXT.some((t) => (i.textContent || '').includes(t)));
      if (target) {
        target.click();
        log('Web search enabled (menu item):', target.textContent.trim());
        return;
      }
      warn('"Web search" item not found in the opened menu');
      document.body.click();
    }

    warn('Web search toggle/item not found: sending without web search');
  }

  function sendMessage() {
    const send = queryFirst(SEND_SELECTORS);
    if (send) {
      send.el.click();
      log('Message sent (button):', send.selector);
      return;
    }
    const input = queryFirst(INPUT_SELECTORS);
    if (!input) {
      warn('No send button and no input: cannot send');
      return;
    }
    input.el.focus();
    const opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
    input.el.dispatchEvent(new KeyboardEvent('keydown', opts));
    input.el.dispatchEvent(new KeyboardEvent('keypress', opts));
    input.el.dispatchEvent(new KeyboardEvent('keyup', opts));
    log('Message sent (Enter)');
  }

  /* ------------------------------------------------------------------ */
  /*  Startup                                                            */
  /* ------------------------------------------------------------------ */
  function main() {
    if (location.hostname === DEEPSEEK_HOST) {
      partB();
    } else {
      partA();
    }
  }

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