// ==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);
});
};
})();