【油猴子】快速问 AI

我想通过快速启动工具向 AI 提问,这样想到什么立刻就问,可能让我思路顺畅一点。

最简单的方法就是通过网址传入自己的问题,但是很多 AI 的网页版不支持这个用法。

那就让他支持!就用 AI 写了个脚本搞这个事情,网址支持两个参数:

  • q 要问的问题
  • m 选择的模型
站点 地址 模型参数(`m=`)
DeepSeek chat.deepseek.com `default` 快速 / `expert` 专家(R1) / `vision` 识图
Kimi www.kimi.com `fast` 快速 / `think` 思考 / `agent` Agent / `cluster` Agent 集群

但是不自动提交,方便修改一下什么的(个人喜好)

脚本……脚本也不重要吧,大家都有 AI,自己按需求定制就行了,反正就是个思路而已

// ==UserScript==
// @name         AI 网页版 Query 参数问答填充
// @namespace    https://github.com/ai-web-query-userscript
// @version      0.2.0
// @description  通过 URL query 参数(q=问题, m=模型)向 AI 网页版输入框自动填入问答内容,仅填充不提交。支持 DeepSeek、Kimi。
// @author       大橘
// @match        https://chat.deepseek.com/*
// @match        https://www.kimi.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ============================================================
  // 区域:适配器注册表 Adapters
  // 每个站点一个适配器配置,新增站点只需在此追加一项
  // ============================================================
  // #region Adapters

  /**
   * DeepSeek 适配器(已通过 CDP 实测验证)
   * - 输入框:textarea,placeholder="给 DeepSeek 发送消息 "
   * - 模型选择:data-model-type 属性,role="radio",激活态 aria-checked="true"
   */
  const deepseekAdapter = {
    id: 'deepseek',                                  // 站点唯一标识
    match: 'https://chat.deepseek.com/*',            // @match 用通配
    inputSelector: [                                 // 输入框选择器,按优先级
      'textarea[placeholder="给 DeepSeek 发送消息"]', // 最稳:中文 placeholder 不易变
      '._24fad49 textarea',                          // 兜底:父级 class
      'textarea'                                     // 最后兜底:页面唯一 textarea
    ],
    inputType: 'textarea',                           // 输入框类型
    // 模型选择器映射:key=参数值, value={ selector }
    // 选择器直接指向可点击的 radio 元素
    modelConfig: {
      default: { selector: '[data-model-type="default"]' }, // 快速模式
      expert:  { selector: '[data-model-type="expert"]'  }, // 专家模式(R1 深度思考)
      vision:  { selector: '[data-model-type="vision"]'  }  // 识图模式
    },
    // 激活态判定:取元素自身属性
    modelIsActive: (el) => el.getAttribute('aria-checked') === 'true'
  };

  /**
   * Kimi 适配器(已通过 CDP 实测验证)
   * - 输入框:contenteditable div,class="chat-input-editor",Lexical 富文本编辑器
   * - 模型选择:点击 .current-model 展开下拉,选项为 .model-item,激活态带 .checked 类
   *   - fast    = K2.6 快速
   *   - think   = K2.6 思考(默认)
   *   - agent   = K2.6 Agent
   *   - cluster = K2.6 Agent 集群
   */
  const kimiAdapter = {
    id: 'kimi',                                      // 站点唯一标识
    match: 'https://www.kimi.com/*',                 // @match 用通配
    inputSelector: [                                 // 输入框选择器,按优先级
      '.chat-input-editor',                          // 主选择器:Lexical 编辑器
      '[data-lexical-editor="true"]',                // 兜底:data 属性
      '[contenteditable="true"].editor'              // 最后兜底
    ],
    inputType: 'contenteditable',                    // 输入框类型:富文本
    // 模型配置:下拉式切换
    // triggerSelector: 展开下拉的按钮
    // optionSelector:  选项选择器(按文本匹配或 class 匹配)
    modelConfig: {
      fast:    { optionText: 'K2.6 快速'    },  // 快速响应
      think:   { optionText: 'K2.6 思考'    },  // 多轮搜索思考
      agent:   { optionText: 'K2.6 Agent'   },  // 调研、PPT、网站、文档、表格
      cluster: { optionText: 'K2.6 Agent 集群' } // 海量搜索、长文写作、批量处理
    },
    // 下拉触发器选择器
    modelTrigger: '.current-model',
    // 激活态判定:从当前显示文本判断,或从下拉项 .checked 判断
    modelIsActive: (el, key, config) => {
      // el 是下拉触发器 .current-model,从文本判断是否已选目标
      const text = el.textContent?.trim() || '';
      return text.includes(config.optionText.replace(/\s*$/, ''));
    },
    // 在展开的下拉中找到对应选项并点击
    modelFindOption: (config) => {
      const items = document.querySelectorAll('.model-item');
      for (const item of items) {
        if (item.textContent?.includes(config.optionText)) {
          return item;
        }
      }
      return null;
    }
  };

  /** 适配器列表:新增站点在此追加即可 */
  const adapters = [deepseekAdapter, kimiAdapter];

  // #endregion Adapters

  // ============================================================
  // 区域:参数解析 ParamParser
  // 从 URL query 读取 q(问题)与 m(模型)
  // ============================================================
  // #region ParamParser

  /**
   * 从当前 URL 读取 query 参数
   * @param {string} name - 参数名
   * @returns {string|null} 解码后的值,不存在则 null
   */
  function getQueryParam(name) {
    const params = new URLSearchParams(window.location.search); // 解析 search 串
    const value = params.get(name);                              // 读取指定参数
    return value ? decodeURIComponent(value) : null;             // 解码并返回(空值返回 null)
  }

  // #endregion ParamParser

  // ============================================================
  // 区域:DOM 工具 DOMUtils
  // 等待元素、填充输入框、点击等基础操作
  // ============================================================
  // #region DOMUtils

  /** 默认等待超时(毫秒) */
  const WAIT_TIMEOUT = 10000;

  /**
   * 等待匹配选择器的元素出现(MutationObserver + 超时兜底)
   * @param {string[]} selectors - 选择器数组,按优先级尝试
   * @param {number} timeout - 超时毫秒
   * @returns {Promise<Element|null>} 命中的元素,超时返回 null
   */
  function waitForElement(selectors, timeout = WAIT_TIMEOUT) {
    return new Promise((resolve) => {
      const found = findFirst(selectors);                         // 立即查找一次
      if (found) { resolve(found); return; }
      const observer = new MutationObserver(() => {              // 监听 DOM 变化
        const el = findFirst(selectors);
        if (el) { observer.disconnect(); resolve(el); }
      });
      observer.observe(document.body, { childList: true, subtree: true });
      setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); // 超时兜底
    });
  }

  /**
   * 按选择器数组优先级查找第一个命中的元素
   * @param {string[]} selectors
   * @returns {Element|null}
   */
  function findFirst(selectors) {
    for (const sel of selectors) {                               // 按优先级遍历
      const el = document.querySelector(sel);                    // 尝试匹配
      if (el) return el;                                         // 命中即返回
    }
    return null;
  }

  /**
   * 向 textarea 写入值(兼容 React 受控组件)
   * 直接 el.value = x 无法触发 React onChange,需用原生 setter + input 事件
   * @param {HTMLTextAreaElement} el
   * @param {string} value
   */
  function setTextareaValue(el, value) {
    const proto = el.constructor.prototype;                      // 取原型
    const setter = Object.getOwnPropertyDescriptor(proto, 'value'); // 取原生 value setter
    if (setter && setter.set) {
      setter.set.call(el, value);                                // 用原生 setter 赋值
    } else {
      el.value = value;                                          // 兜底直接赋值
    }
    el.dispatchEvent(new Event('input', { bubbles: true }));     // 触发 input 事件
    el.dispatchEvent(new Event('change', { bubbles: true }));    // 触发 change 事件
  }

  /**
   * 向 contenteditable 富文本编辑器写入值
   * 使用 execCommand('insertText') 方式,兼容 Lexical/Slate/ProseMirror 等
   * @param {HTMLElement} el - contenteditable 元素
   * @param {string} value - 要填入的文本
   */
  function setContentEditableValue(el, value) {
    el.focus();                                                  // 聚焦编辑器
    // 全选后用 insertText 替换,确保 Lexical 等框架能感知变更
    document.execCommand('selectAll', false, null);              // 全选现有内容
    document.execCommand('insertText', false, value);            // 插入新文本(替代选中内容)
    el.dispatchEvent(new Event('input', { bubbles: true }));     // 触发 input 事件
    // 光标移到末尾
    const range = document.createRange();
    range.selectNodeContents(el);
    range.collapse(false);
    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
  }

  /**
   * 根据 inputType 选择填充方式并写入值
   * @param {Element} el - 输入框元素
   * @param {string} type - 'textarea' | 'contenteditable'
   * @param {string} value - 要填入的内容
   */
  function fillInput(el, type, value) {
    if (type === 'textarea') {
      setTextareaValue(el, value);
    } else if (type === 'contenteditable') {
      setContentEditableValue(el, value);
    }
  }

  // #endregion DOMUtils

  // ============================================================
  // 区域:模型切换 ModelSwitcher
  // 支持两种模式:直接点击型(DeepSeek)与下拉展开型(Kimi)
  // ============================================================
  // #region ModelSwitcher

  /**
   * 切换模型模式
   * @param {object} adapter - 适配器配置
   * @param {string} modelKey - 参数 m 的值
   * @returns {Promise<boolean>} 是否执行了切换(已是当前模型返回 false)
   */
  async function switchModel(adapter, modelKey) {
    if (!adapter.modelConfig) return false;                     // 不支持模型切换
    const config = adapter.modelConfig[modelKey];
    if (!config) {                                              // 未知模型 key
      console.warn(`[AI Query Filler] 未知模型参数 m=${modelKey},支持: ${Object.keys(adapter.modelConfig).join('/')}`);
      return false;
    }

    // ---- 下拉式切换(Kimi 模式):有 trigger 先点展开 ----
    if (adapter.modelTrigger) {
      const trigger = await waitForElement([adapter.modelTrigger], 5000);
      if (!trigger) {
        console.warn(`[AI Query Filler] 未找到模型切换按钮: ${adapter.modelTrigger}`);
        return false;
      }
      // 检查是否已是目标模型
      if (adapter.modelIsActive && adapter.modelIsActive(trigger, modelKey, config)) {
        console.log(`[AI Query Filler] 模型已是 ${modelKey},无需切换`);
        return false;
      }
      // 点击展开下拉
      trigger.click();
      // 等待下拉项出现
      await new Promise(r => setTimeout(r, 300));
      // 在下拉中找到并点击目标选项
      const option = adapter.modelFindOption
        ? adapter.modelFindOption(config)
        : document.querySelector(config.selector);
      if (option) {
        option.click();
        console.log(`[AI Query Filler] 已切换模型: ${modelKey}`);
        return true;
      } else {
        console.warn(`[AI Query Filler] 未找到模型选项: ${config.optionText || modelKey}`);
        // 点击其他位置收起下拉
        trigger.click();
        return false;
      }
    }

    // ---- 直接点击型(DeepSeek 模式):直接点击目标按钮 ----
    const btn = await waitForElement([config.selector], 5000);
    if (!btn) {
      console.warn(`[AI Query Filler] 未找到模型按钮: ${config.selector}`);
      return false;
    }
    // 检查激活态
    if (adapter.modelIsActive && adapter.modelIsActive(btn, modelKey, config)) {
      console.log(`[AI Query Filler] 模型已是 ${modelKey},无需切换`);
      return false;
    }
    btn.click();
    console.log(`[AI Query Filler] 已切换模型: ${modelKey}`);
    return true;
  }

  // #endregion ModelSwitcher

  // ============================================================
  // 区域:主流程 Main
  // 编排:解析参数 → 匹配适配器 → 切换模型 → 填充输入框
  // ============================================================
  // #region Main

  /** 仅填充一次的标志位 */
  let filled = false;

  /**
   * 主入口
   */
  async function main() {
    // 1. 读取参数:q=问题, m=模型
    const question = getQueryParam('q');
    if (!question) return;                                       // 无问题参数则静默退出

    const modelKey = getQueryParam('m');                         // 模型参数可选

    // 2. 匹配当前站点的适配器
    const adapter = adapters.find(a => window.location.href.startsWith(a.match.replace(/\*$/, '')));
    if (!adapter) return;                                        // 无匹配适配器则退出

    console.log(`[AI Query Filler] 命中适配器: ${adapter.id}, q=${question.slice(0, 30)}..., m=${modelKey || '(默认)'}`);

    // 3. 切换模型(若指定了 m 参数)
    if (modelKey) {
      await switchModel(adapter, modelKey);
      // 切换模型后输入框可能被重建,稍等一下
      await new Promise(r => setTimeout(r, 500));
    }

    // 4. 等待输入框出现
    const input = await waitForElement(adapter.inputSelector);
    if (!input) {
      console.warn('[AI Query Filler] 等待输入框超时');
      return;
    }

    // 5. 填充问题内容(仅一次)
    if (filled) return;
    filled = true;

    fillInput(input, adapter.inputType, question);              // 根据类型填充
    input.focus();                                               // 聚焦输入框

    console.log('[AI Query Filler] 已填充问题到输入框(未提交)');
  }

  // #endregion Main

  // 启动
  main().catch(err => console.error('[AI Query Filler] 异常:', err));

})();