StegLLM

此脚本已不再维护,最新项目详见https://github.com/Rin313/StegLLM。This script is no longer maintenance, please refer to the https://github.com/Rin313/StegLLM for the latest project

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         StegLLM
// @namespace    https://github.com/Rin313
// @version      1.03
// @description  此脚本已不再维护,最新项目详见https://github.com/Rin313/StegLLM。This script is no longer maintenance, please refer to the https://github.com/Rin313/StegLLM for the latest project
// @author       Rin
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/xxhash-wasm.min.js
// ==/UserScript==
(function() {
    'use strict';
    const hostname=window.location.hostname;
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    window.onerror = function(message, source, lineno, colno, error) {
      if(!source)return;
      if(source.includes("StegLLM"))
        alert(`Error:${error.message}`);
    }
    const createElement = (tag, props = {}, styles = {}) => {
        const el = Object.assign(document.createElement(tag), props);//创建元素
        Object.assign(el.style, styles);//配置styles
        return el;
    };
    let settings = {
        prompt: GM_getValue("prompt", '续写这段散文:'),//如果不存在则使用默认值
    };
    GM_registerMenuCommand('prompt setting', function() {
        let customPrompt = prompt("", settings.prompt);
        if (customPrompt) {
          GM_setValue("prompt", customPrompt);
          settings.prompt=customPrompt;
        }
    });
    const gbkDecoder = new TextDecoder('gb18030');//能解码gbk不支持的符号,比如欧元、表意文字
    const utf8Encoder= new TextEncoder();
    const ranges = [
      [0xA1, 0xA9,  0xA1, 0xFE],
      [0xB0, 0xF7,  0xA1, 0xFE],
      [0x81, 0xA0,  0x40, 0xFE],//从这里开始的三个扩展区,第二个字节要排除0x7F
      [0xAA, 0xFE,  0x40, 0xA0],
      [0xA8, 0xA9,  0x40, 0xA0],
    ];
    let codes,table;
    const punctuations=["?","?","!","!","。",")",")","……"];//,"\n"
    const logitBias=[[" ",false],[" ",false],["   ",false],["\n\n",false],["  \n",false],[" \n",false],["�",false],[" �",false],[".",false],["【",false],["】",false],["〈",false],["〉",false]]
    const intercept=2;
    const tokens=1;// const tokens=Math.ceil(intercept/0.75);
    const probs=10;
    function shuffle(array) {
      for (let i = array.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1)); // 生成 0 到 i 之间的随机整数
          [array[i], array[j]] = [array[j], array[i]]; // 交换元素
      }
      return array;
    }
    function encodeToGBK(str) {
      if(!codes){
        codes=new Uint16Array(22046);//先把全部gbk字符都保存到一个16位整型数组里
        let i = 0,t;
        for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
          for (let b2 = b2Begin; b2 <= b2End; b2++) {
            if (b2 !== 0x7F) {//反过来遍历,减少判断0x7F的次数
              t = b2 << 8; //不能用16位的codes[i]
              for (let b1 = b1Begin; b1 <= b1End; b1++)
                codes[i++] = t | b1;
            }
          }
        }
      }
      if(!table){
        table = new Uint16Array(65509);//gbk包含¤164,将164左移到0也才省一点点空间
        const str = gbkDecoder.decode(codes);//解码为包含全部gbk字符的字符串
        for (let i = 0; i < str.length; i++){
          table[str.charCodeAt(i)] = codes[i];//unicode到gbk的映射
        }
      }
      const buf = new Uint8Array(str.length * 2);
      let n = 0;
      for (let i = 0; i < str.length; i++) {
        const code = str.charCodeAt(i);
        if (code < 128)
          buf[n++] = code;
        else{
              const gbk = table[code];
              if (gbk === 0)
                throw new Error("文本中存在不支持的符号");//有些编码器会用问号替换来避免报错,但这实际已经发生信息丢失了,不能容忍
              else {
                buf[n++] = gbk;
                buf[n++] = gbk >> 8;
              }
        }
      }
      return buf.subarray(0, n);
    }
    async function readStream(stream) {
      const reader = stream.getReader();
      const chunks = [];
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          break;
        }
        chunks.push(value);
      }
      const compressedData = new Uint8Array(chunks.reduce((acc, val) => acc + val.length, 0));
      let offset = 0;
      for (const chunk of chunks) {
        compressedData.set(chunk, offset);
        offset += chunk.length;
      }
      return compressedData;
    }
    async function decompress(stream) {
      const ds = new DecompressionStream("deflate-raw");
      const decompressedStream = stream.pipeThrough(ds);
      return readStream(decompressedStream).then(data => {
        return data; // 或者在这里进行一些额外的处理
      });
    }
    // async function passwordToAesCtrKey(password) {
    //   const passwordBuffer = utf8Encoder.encode(password);
    //   // 使用 PBKDF2 算法从密码派生密钥
    //   const keyMaterial = await crypto.subtle.importKey(
    //     "raw",
    //     passwordBuffer,
    //     { name: "PBKDF2" },
    //     false,
    //     ["deriveKey"]
    //   );
    //   // 使用 PBKDF2 派生 AES-CTR 密钥
    //   const aesCtrKey = await crypto.subtle.deriveKey(
    //     {
    //     name: "PBKDF2",
    //     salt: new Uint8Array(0), // 空盐值
    //     iterations: 1000, // 较低的迭代次数
    //     hash: "SHA-256",
    //     },
    //     keyMaterial,
    //     { name: "AES-CTR", length: 256 }, // 指定 AES-CTR 算法和密钥长度 (256位)
    //     true, // 密钥可导出
    //     ["encrypt", "decrypt"] // 密钥用途
    //   );
    //   return aesCtrKey;
    // }
    async function encryptAesCtr(data, str) {
      const buffer=await crypto.subtle.digest('SHA-256', utf8Encoder.encode(str));
      const iv=new Uint8Array(buffer).subarray(0, 16);
      const key= await crypto.subtle.importKey(
        "raw",
        buffer,
        { name: "AES-CTR", length: 256},
        false,
        ["encrypt", "decrypt"]
      );
      const encrypted = await crypto.subtle.encrypt(
        {
          name: "AES-CTR",
          counter: iv,
          length: 64, // 计数器块大小(以位为单位),通常为 64 或 128
        },
        key,data
      );
      return new Uint8Array(encrypted);
    }
    async function decryptAesCtr(data, str) {
      const buffer=await crypto.subtle.digest('SHA-256', utf8Encoder.encode(str));
      const iv=new Uint8Array(buffer).subarray(0, 16);
      const key= await crypto.subtle.importKey(
        "raw",
        buffer,
        { name: "AES-CTR", length: 256},
        false,
        ["encrypt", "decrypt"]
      );
      const decrypted = await crypto.subtle.decrypt(
        {
        name: "AES-CTR",
        counter: iv,
        length: 64,
        },
        key,
        data
      );
      return new Uint8Array(decrypted);
    }
    async function chat(str,complete=false) {
      const body={//有些参数不生效,响应格式也和llama.cpp的api略有不同//在api中设置system_prompt会导致性能严重下降
          // "stream": true,
          "n_predict": tokens,//生成的token数,-1-2048
          "temperature": 1.4,//影响文本的随机性,0-2//较高的温度会增加计算量,较低的温度会导致重复
          // "stop": punctuations,
          "repeat_last_n": 256,
          "repeat_penalty": 1.18,//重复惩罚,1.0为无惩罚
          // "top_p": 0.95,//默认0.95,增大后似乎能增加更多的选词可能性
          // "min_p": 0.05,
          // "tfs_z": 1,
          // "typical_p": 1,
          // "presence_penalty": 0,
          // "frequency_penalty": 0,
          // "mirostat": 0,//关闭mirostat
          // "mirostat_tau": 5,
          // "mirostat_eta": 0.1,
          // "grammar": "",
          // "min_keep": 0,
          // "image_data": [],
          "cache_prompt": true,//提示词复用
          "api_key": "",
          "slot_id": -1,
          "prompt": str,//支持输入多个prompt
          // "response_fields": ["content"],//不生效?
          "top_k": probs,//选词范围,默认40
          "n_probs": probs,//按概率排序的前10个选词,太大或太小都会降低隐写效果
          "logit_bias": logitBias//禁用一些不自然的字符,注意空白符有非常多种
      }
      if(complete){
        body["n_predict"]=9;
        body["stop"]=punctuations;//动态截断
        body["n_probs"]=0;
        body["top_k"]=40;
      }
      const response = await fetch('http://localhost:8080/completion', {
        method: 'POST',
        body: JSON.stringify(body)
      });
      if(!response.ok)
        throw new Error(`HTTP error! status: ${response.status}`);
      const json=(await response.json());
      if(complete)
        return json.content+json.stopping_word;
      const t=json.completion_probabilities[0];
      if(!t)return chat(str);
      else return shuffle(t.probs);
    }
    async function encrypt() {
        const plainText = (await createCustomPrompt("🔒"));
        if(plainText){
          const { h32 } = await xxhash();
          let bytes= encodeToGBK(plainText);
          console.log(bytes);
          const stream=new ReadableStream({
            start(controller) {
              controller.enqueue(bytes);
              controller.close();
            }
          });
          const compressedStream = stream.pipeThrough(new CompressionStream("deflate-raw"),);
          const result=await readStream(compressedStream);
          if(bytes.length>result.length)
            bytes=result;
          console.log(bytes);
          bytes=(await encryptAesCtr(bytes,hostname));
          console.log(bytes);
          let base2=[];
          for (let b of bytes) {
            for(let i=7;i>=0;i--){
              base2.push(b>>i & 0x01);
            }
          }
          console.log(base2);
          const counts=new Uint8Array(base2.length);
          let coverText='';
          for(let i=0;i<base2.length;i++){
            const list=(await chat(settings.prompt+coverText));
            let j=0;
            aaa:for(;j<list.length;j++){
              const t=list[j].tok_str;
              if(t.length===2&&!t.includes("\uFFFD")&&h32(t)%2===base2[i]){
                coverText+=t;
                break;
              }
              else if(t.length===1){
                const list2=(await chat(settings.prompt+coverText+t));
                for(let k=0;k<list2.length;k++){
                  const t2=list2[k].tok_str;
                  if(t2.length===1&&!t2.includes("\uFFFD")&&h32(t+t2)%2===base2[i]){
                    coverText+=t+t2;
                    break aaa;
                  }
                }
              }
            }
            if(j===list.length){
              alert("选词失败,请重新再试");
              return;
            }

          }
          console.log(coverText.length);
          if(!punctuations.includes(coverText[coverText.length-1])){
            for(;;){
              const t=(await chat(settings.prompt+coverText,true));
              if(punctuations.includes(t[t.length-1])){
                coverText+=t;
                break;
              }
            }
          }
          showCustomAlert(coverText);
        }
    }
    async function decrypt() {
      const userInput=(await createCustomPrompt("🗝️"));//粘贴到prompt会导致空白字符等丢失//粘贴到input会导致换行符丢失
      if(userInput){
        const { h32 } = await xxhash();
        let base2 = [];
        let t='';
        //console.log(userInput)
        console.log(userInput.length);
        for (let i = 0; i < userInput.length; i++) {
          t+=userInput[i];
          if(t.length===intercept){
            //console.log(t+" "+t.length)
            base2.push(h32(t)%2);
            t='';
          }
        }
        let bytes = new Uint8Array(base2.length/8);let k=0;
        console.log(base2);
        for(let i=0;i<base2.length;){
          bytes[k++]=base2[i]*128+base2[i+1]*64+base2[i+2]*32+base2[i+3]*16+base2[i+4]*8+base2[i+5]*4+base2[i+6]*2+base2[i+7];
          i+=8;
        }
        console.log(bytes)
        bytes=(await decryptAesCtr(bytes,hostname));
        console.log(bytes)
        const stream=new ReadableStream({
            start(controller) {
              controller.enqueue(bytes);
              controller.close();
            }
        });
        await decompress(stream)
          .then(data=>{bytes=data;})
          .catch(error=>{console.log(error)});
        console.log(bytes)
        alert(gbkDecoder.decode(bytes));
      }
    }
    function swapColors(){
      let t=sidebarButton1.style.backgroundColor;
      sidebarButton1.style.backgroundColor=sidebarButton2.style.backgroundColor;
      sidebarButton2.style.backgroundColor=t;
    }
    const buttonStyles1 = {
      position: 'fixed',
      right: '0', //固定右侧
      zIndex: '9999', // 确保不被覆盖
      cursor: 'pointer',//显示可点击光标
      backgroundColor:'#f56c73',
      border: 'none',
      top:   '42%',
      height: '25px',
      width: '25px',
      overflow: 'hidden',
    };
    const buttonStyles2 = {
        position: 'fixed',
        right: '0', //固定右侧
        zIndex: '9999', // 确保不被覆盖
        cursor: 'pointer',//显示可点击光标
        backgroundColor:'#d87b83',
        border: 'none',
        top:   '47%',
        height: '25px',
        width: '25px',
        overflow: 'hidden',
      };
    const sidebarButton1 = createElement('button', {}, buttonStyles1);
    const sidebarButton2 = createElement('button', {}, buttonStyles2);
    sidebarButton1.addEventListener('mouseenter', () => swapColors() );
    sidebarButton2.addEventListener('mouseenter', () => swapColors() );
    sidebarButton1.addEventListener('click', () => encrypt());
    sidebarButton2.addEventListener('click', () => decrypt());
    document.body.append(sidebarButton1, sidebarButton2);
const showCustomAlert = (text) => {
    // 创建遮罩层
    const overlay = createElement('div', {}, {
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        zIndex: 9999,
    });

    // 创建弹出框容器
    const alertBox = createElement('div', {}, {
        backgroundColor: '#fff',
        padding: '20px',
        borderRadius: '8px',
        boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)',
        textAlign: 'center',
        width: '300px',
    });

    // 创建显示的文本
    const message = createElement('p', { textContent: text }, {
        margin: '0 0 20px',
        fontSize: '16px',
        color: '#333',
        wordBreak: 'break-word',
    });

    // 创建按钮容器
    const buttonContainer = createElement('div', {}, {
        display: 'flex',
        justifyContent: 'space-between',
        marginTop: '20px',
    });

    // 创建复制按钮
    const copyButton = createElement('button', { textContent: 'Copy' }, {
        padding: '10px 20px',
        backgroundColor: '#007bff',
        color: '#fff',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        fontSize: '14px',
        flex: '1',
        marginRight: '10px',
    });

    // 按钮点击事件 - 复制文本
    copyButton.onclick = () => {
        navigator.clipboard.writeText(text).then(() => {
            alert('Copied to clipboard!');
            document.body.removeChild(overlay);
        });
    };

    // 创建关闭按钮
    const closeButton = createElement('button', { textContent: 'Close' }, {
        padding: '10px 20px',
        backgroundColor: '#dc3545',
        color: '#fff',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        fontSize: '14px',
        flex: '1',
    });

    // 关闭按钮点击事件
    closeButton.onclick = () => {
        document.body.removeChild(overlay);
    };

    // 组装按钮容器
    buttonContainer.appendChild(copyButton);
    buttonContainer.appendChild(closeButton);

    // 组装弹出框
    alertBox.appendChild(message);
    alertBox.appendChild(buttonContainer);
    overlay.appendChild(alertBox);
    document.body.appendChild(overlay);
};
const createCustomPrompt = (placeholder = "请输入内容...") => {
  return new Promise((resolve) => {
    // 创建一个覆盖整个页面的背景遮罩
    const overlay = createElement('div', {}, {
      position: 'fixed',
      top: '0',
      left: '0',
      width: '100vw',
      height: '100vh',
      backgroundColor: 'rgba(0, 0, 0, 0.5)',
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      zIndex: '1000',
    });

    // 创建一个容器来放置textarea和按钮
    const promptContainer = createElement('div', {}, {
      backgroundColor: '#fff',
      padding: '20px',
      borderRadius: '8px',
      boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)',
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      minWidth: '300px',
    });

    // 创建textarea
    const textarea = createElement(
      'textarea',
      {
        placeholder: placeholder,
      },
      {
        width: '100%',
        height: '100px',
        marginBottom: '10px',
        padding: '10px',
        fontSize: '16px',
        borderRadius: '4px',
        border: '1px solid #ddd',
        outline: 'none',
        resize: 'none',
      }
    );

    // 创建提交按钮
    const submitButton = createElement(
      'button',
      {
        innerText: '提交',
        onclick: () => {
          const value = textarea.value;
          resolve(value); // 当点击提交时,resolve Promise 并返回值
          document.body.removeChild(overlay); // 移除遮罩层
        },
      },
      {
        padding: '10px 20px',
        fontSize: '16px',
        backgroundColor: '#007BFF',
        color: '#fff',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
      }
    );

    // 提交按钮 hover 样式
    submitButton.addEventListener('mouseover', () => {
      submitButton.style.backgroundColor = '#0056b3';
    });
    submitButton.addEventListener('mouseout', () => {
      submitButton.style.backgroundColor = '#007BFF';
    });
    overlay.addEventListener('click', (event) => {
      if (event.target === overlay) {
        resolve(null); // 用户取消操作时,返回 null
        document.body.removeChild(overlay); // 移除遮罩层
      }
    });
    // 把textarea和按钮添加到容器中
    promptContainer.appendChild(textarea);
    promptContainer.appendChild(submitButton);

    // 把容器添加到遮罩层中
    overlay.appendChild(promptContainer);

    // 把遮罩层添加到body中
    document.body.appendChild(overlay);
  });
};
})();