// ==UserScript==
// @namespace https://greasyfork.org/zh-CN/users/1106595-ikkem-lin
// @name GPT Auto task
// @author Mark
// @description 根据缓存中的数据自动在网页上与chat gpt对话
// @homepageURL https://github.com/IKKEM-Lin/gpt-auto-task
// @version 0.2.0
// @match *chat.openai.com/*
// @run-at document-idle
// @require https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js
// @require https://cdn.jsdelivr.net/npm/idb-keyval@6/dist/umd.js
// ==/UserScript==
(function () {
"use strict";
const tableName = "data";
const dbTable = {
tasks: idbKeyval.createStore("tasks", tableName),
config: idbKeyval.createStore("config", tableName),
skipSnippet: idbKeyval.createStore("skipSnippet", tableName),
response1: idbKeyval.createStore("response1", tableName),
response2: idbKeyval.createStore("response2", tableName),
responseProcessed: idbKeyval.createStore("responseProcessed", tableName),
};
const locationReload = () => {
location.href = "https://chat.openai.com/?model=gpt-4";
};
const downloadFile = (data, fileName) => {
const a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
const blob = new Blob([data], {
type: "application/octet-stream",
});
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
};
const yaml2object = (yamlStr) => {
try {
return jsyaml.load(yamlStr);
} catch (error) {
return null;
}
};
function hashFnv32a(str, asString = true, seed = undefined) {
/*jshint bitwise:false */
var i,
l,
hval = seed === undefined ? 0x811c9dc5 : seed;
for (i = 0, l = str.length; i < l; i++) {
hval ^= str.charCodeAt(i);
hval +=
(hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
if (asString) {
// Convert to 8 digit hex string
return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
}
return hval >>> 0;
}
function reactionObjHandler(input) {
let result = [];
const validateKeys = ["reactants", "products", "condition", "catalysts"];
function test(reaction) {
// if (!reaction) {
// return
// }
if (reaction instanceof Array) {
return reaction.map((item) => {
return test(item);
});
} else {
try {
var keys = Object.keys(reaction);
} catch (error) {
// debugger;
console.error(error);
throw new Error();
}
if (validateKeys.some((key) => keys.includes(key))) {
result.push(reaction);
return;
}
keys.forEach((key) => {
if (reaction[key] && typeof reaction[key] === "object") {
test(reaction[key]);
}
});
}
}
test(input);
return result;
}
function readFile(accept = "", multiple = false) {
const inputEl = document.createElement("input");
inputEl.setAttribute("type", "file");
inputEl.setAttribute("accept", accept);
inputEl.setAttribute("multiple", !!multiple);
return new Promise((resolve, reject) => {
inputEl.addEventListener("change", (e) => {
resolve(multiple ? inputEl.files : inputEl.files[0]);
window.removeEventListener("click", onWindowClick, true);
});
inputEl.click();
const onWindowClick = () => {
if (!inputEl.value) {
reject(new Error("用户取消选择"));
}
window.removeEventListener("click", onWindowClick, true);
};
setTimeout(() => {
window.addEventListener("click", onWindowClick, true);
}, 100);
});
}
class GPT_ASK_LOOP {
queue = [];
abstract = [];
responds = [];
checkInterval = 20000;
account = "";
downloadBtn = null;
retrying = false;
lastSaveTime = 0;
prompt1 = "";
prompt2 = "";
modelNum = 1;
INPUT_SELECTOR = "#prompt-textarea";
SUBMIT_BTN_SELECTOR = 'button[data-testid="send-button"]';
RESPOND_SELECTOR = 'main div[data-message-author-role="assistant"]';
NEW_CHART_BTN_SELECTOR = "nav div.flex-col a[href='/']";
NORMAL_RESPOND_BTN_SELECTOR = "form div button.btn-neutral";
ERROR_RESPOND_BTN_SELECTOR = "form div button.btn-primary";
sleep(duration) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, duration);
});
}
constructor(account) {
this.initData().then(() => {
this.account = account || Math.ceil(Math.random() * 1e10).toString(32);
const btnWrap = document.createElement("div");
btnWrap.innerHTML = `<button style="padding: 4px 8px;position: fixed;bottom: 20%;right: 8px;border-radius: 4px;background-color: #224466;color: #fff;">下载已生成结果(queue: ${this.queue.length}, res: ${this.responds.length})</button>`;
this.downloadBtn = btnWrap.querySelector("button");
this.downloadBtn.onclick = this.handleDownload.bind(this);
document.body.appendChild(btnWrap);
const btnWrapBackup = document.createElement("div");
btnWrapBackup.innerHTML = `<button style="padding: 4px 8px;position: fixed;bottom: 30%;right: 8px;border-radius: 4px;background-color: #224466;color: #fff;">备份</button>`;
const backupBtn = btnWrapBackup.querySelector("button");
backupBtn.onclick = this.backUp.bind(this);
document.body.appendChild(btnWrapBackup);
const btnWrapImport = document.createElement("div");
btnWrapImport.innerHTML = `<button style="padding: 4px 8px;position: fixed;bottom: 40%;right: 8px;border-radius: 4px;background-color: #224466;color: #fff;">导入</button>`;
const importBtn = btnWrapImport.querySelector("button");
importBtn.onclick = async () => {
if (
!window.confirm(
"The data in browser will be clear up. Please make sure you have to do this !!!"
)
) {
return;
}
const file = await readFile(".json");
const reader = new FileReader();
reader.onload = (event) => {
const json = JSON.parse(event.target.result);
// console.log({json}, 'json')
this.importFromBackUp.bind(this)(json);
};
reader.readAsText(file);
};
document.body.appendChild(btnWrapImport);
this.main();
});
}
async initData() {
await this.legacyTaskInit();
const skipSnippetKeys = await idbKeyval.keys(dbTable.skipSnippet);
const responseKeys = await idbKeyval.keys(dbTable.responseProcessed);
const responseValues = await idbKeyval.values(dbTable.responseProcessed);
this.responds = responseValues.map((item) => ({
articleId: item.articleId,
snippetId: item.snippetId,
createdTime: item.createdTime,
}));
const indexDBConfig = (await idbKeyval.entries(dbTable.config)) || [];
if (indexDBConfig.length) {
this.prompt1 = indexDBConfig.find((item) => item[0] === "prompt1");
this.prompt2 = indexDBConfig.find((item) => item[0] === "prompt2");
this.modelNum = indexDBConfig.find((item) => item[0] === "modelNum");
this.prompt1 = (this.prompt1 && this.prompt1[1]) || "";
this.prompt2 = (this.prompt2 && this.prompt2[1]) || "";
this.modelNum = (this.modelNum && this.modelNum[1]) || 1;
}
const snippetSourceData = (await idbKeyval.values(dbTable.tasks)) || [];
this.abstract = snippetSourceData.filter(
(item) => item.type == "abstract"
);
const paragraphs = snippetSourceData.filter(
(item) => item.type != "abstract"
);
this.queue = paragraphs.filter(
(item) =>
!(responseKeys || []).includes(`${item.article_id}-${item.id}`) &&
!(skipSnippetKeys || []).includes(`${item.article_id}-${item.id}`)
);
if (this.queue.length !== 0) {
return;
}
this.queue = paragraphs.filter((item) =>
(skipSnippetKeys || []).includes(`${item.article_id}-${item.id}`)
);
const skipSnippetEntries = await idbKeyval.entries(dbTable.skipSnippet);
this.queue.sort((a,b) => {
const aItem = skipSnippetEntries.find(item => item[0] === `${a.article_id}-${a.id}`)
const bItem = skipSnippetEntries.find(item => item[0] === `${b.article_id}-${b.id}`)
return aItem[1] < bItem[1] ? -1 : 1
})
}
async legacyTaskInit() {
const indexDBTasks = (await idbKeyval.entries(dbTable.tasks)) || [];
const indexDBConfig = (await idbKeyval.entries(dbTable.config)) || [];
if (indexDBConfig.length === 0) {
const prompt1 = localStorage.getItem("mock_prompt1");
const prompt2 = localStorage.getItem("mock_prompt2");
const modelNum = +localStorage.getItem("model_number") || 1;
if (!prompt1 || !prompt2) {
return;
}
await idbKeyval.setMany(
[
["prompt1", prompt1],
["prompt2", prompt2],
["modelNum", modelNum],
],
dbTable.config
);
}
if (indexDBTasks.length === 0) {
const snippetSourceData = JSON.parse(
localStorage.getItem("snippetSourceData") || "[]"
);
if (!snippetSourceData.length) {
return;
}
const snippetSourceDataEntries = snippetSourceData.map((item) => [
`${item.article_id}-${item.id}`,
item,
]);
await idbKeyval.setMany(snippetSourceDataEntries, dbTable.tasks);
}
}
async importFromBackUp(data) {
const {
response1,
response2,
responseProcessed,
skipSnippet,
config,
tasks,
} = data;
if (
!(
response1 &&
response2 &&
responseProcessed &&
skipSnippet &&
config &&
tasks
)
) {
alert(
`[ "response1", "response2", "responseProcessed", "skipSnippet", "config", "tasks" ], all of them are required`
);
return;
}
await idbKeyval.clear(dbTable.response1);
await idbKeyval.clear(dbTable.response2);
await idbKeyval.clear(dbTable.responseProcessed);
await idbKeyval.clear(dbTable.skipSnippet);
await idbKeyval.clear(dbTable.config);
await idbKeyval.clear(dbTable.tasks);
await idbKeyval.setMany(response1, dbTable.response1);
await idbKeyval.setMany(response2, dbTable.response2);
await idbKeyval.setMany(responseProcessed, dbTable.responseProcessed);
await idbKeyval.setMany(skipSnippet, dbTable.skipSnippet);
await idbKeyval.setMany(config, dbTable.config);
await idbKeyval.setMany(tasks, dbTable.tasks);
locationReload();
}
async backUp() {
const response1 = (await idbKeyval.entries(dbTable.response1)) || [];
const response2 = (await idbKeyval.entries(dbTable.response2)) || [];
const responseProcessed =
(await idbKeyval.entries(dbTable.responseProcessed)) || [];
const skipSnippet = (await idbKeyval.entries(dbTable.skipSnippet)) || [];
const config = (await idbKeyval.entries(dbTable.config)) || [];
const tasks = (await idbKeyval.entries(dbTable.tasks)) || [];
const paragraphs = tasks.filter((item) => item[1].type != "abstract");
const articleIds = tasks.map((item) => item[1].article_id).sort();
const now = new Date();
const current = `${now.getFullYear()}-${
now.getMonth() + 1
}-${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}`;
downloadFile(
JSON.stringify({
response1,
response2,
responseProcessed,
skipSnippet,
config,
tasks,
}),
`Article_${articleIds[0]}_${
articleIds[articleIds.length - 1]
}-progress_${paragraphs.length}_${
responseProcessed.length
}-${current}.backup.json`
);
}
async handleDownload() {
const reactionGroups = await idbKeyval.values(dbTable.responseProcessed);
if (!reactionGroups.length) {
return;
}
const reactions = [];
reactionGroups.forEach((item) => {
const { articleId, snippetId, reaction } = item;
const uniqReaction = Array.from(
new Set(reaction.map((v) => JSON.stringify(v)))
).map((v) => JSON.parse(v));
uniqReaction.forEach((data) => {
const name = hashFnv32a(JSON.stringify(data));
reactions.push({ articleId, snippetId, data, name });
});
});
const now = new Date();
downloadFile(
JSON.stringify(reactions),
`${this.account}-${now.getFullYear()}-${
now.getMonth() + 1
}-${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}-${
reactions.length
}.json`
);
}
async report(tip = "") {
await fetch("http://localhost:3000", {
method: "POST",
body: JSON.stringify({
account: this.account,
reaction_count: this.responds.length,
queue_count: this.queue.length,
tip: tip,
}),
}).catch((err) => {
// console.error({ err });
});
}
genPrompt(content, step = 1) {
return step === 1
? `${this.prompt1}
''' ${content} ''' `
: this.prompt2;
}
async _updateDownloadBtnText() {
if (this.downloadBtn) {
const snippetSourceData = (await idbKeyval.values(dbTable.tasks)) || [];
const paragraphs = snippetSourceData.filter(
(item) => item.type != "abstract"
);
this.downloadBtn.innerText = `下载已生成结果(queue: ${
this.queue.length
}, res: ${this.responds.length}, skip: ${
paragraphs.length - this.queue.length - this.responds.length
})`;
}
}
_getLastRespondTime() {
return Math.max.apply(
null,
this.responds
.map((item) => item.createdTime)
.filter((item) => item)
.concat([0])
);
}
getTask() {
const task = this.queue[0];
const maxTime = this._getLastRespondTime();
this.report(
(task &&
`Working on articleId: ${task.article_id}, snippetId: ${
task.id
}, last-update-time: ${new Date(maxTime).toLocaleString()}`) ||
""
);
if (!task) {
console.log("任务队列为空");
return async () => null;
}
return async () => {
const { article_id, id, content } = task;
const relatedAbstract =
this.abstract.find((item) => item.article_id === article_id)
?.content || "";
console.log(
`开始触发 ${article_id}-${id}, ${new Date().toTimeString()}`
);
const promptContent = `
${relatedAbstract}
${content}
`;
const prompt1 = this.genPrompt(promptContent, 1);
const prompt2 = this.genPrompt(promptContent, 2);
const result1 = await this.trigger(prompt1).catch((err) => {
return null;
});
if (!result1) {
return null;
}
await idbKeyval.set(
`${article_id}-${id}`,
{
articleId: article_id,
snippetId: id,
reaction: result1,
createdTime: new Date().valueOf(),
},
dbTable.response1
);
await this.sleep(3 * 1000);
const result2 = await this.trigger(prompt2).catch((err) => {
return null;
});
if (!result2) {
return { articleId: article_id, snippetId: id, reaction: result1 };
}
await idbKeyval.set(
`${article_id}-${id}`,
{
articleId: article_id,
snippetId: id,
reaction: result2,
createdTime: new Date().valueOf(),
},
dbTable.response2
);
return { articleId: article_id, snippetId: id, reaction: result2 };
};
}
async rawReactionProcess(rawReactionHTML) {
const ele = document.createElement("div");
ele.innerHTML = rawReactionHTML;
const res = Array.from(ele.querySelectorAll("code"))
.map((el) => el.innerText)
.map((yml) => yaml2object(yml));
if (res && res.length > 0 && res.every((s) => s !== null)) {
const result = reactionObjHandler(res);
return result.length > 0 ? result : null;
}
return null;
}
async skipSnippetHandler(articleId, snippetId) {
const oldVal = await idbKeyval.get(
`${articleId}-${snippetId}`,
dbTable.skipSnippet
);
await idbKeyval.set(
`${articleId}-${snippetId}`,
(oldVal || 0) + 1,
dbTable.skipSnippet
);
this.queue = this.queue.filter((item) => item.id !== snippetId);
}
async saveRespond(respond) {
const { articleId, snippetId } = respond;
const currentTimeStamp = new Date().valueOf();
const reactionProcessed = await this.rawReactionProcess(respond.reaction);
if (!reactionProcessed) {
console.warn(`${articleId}-${snippetId} 无法解析出 reaction, 即将跳过`);
await this.skipSnippetHandler(articleId, snippetId);
return;
}
this.responds.push({
articleId,
snippetId,
createdTime: currentTimeStamp,
});
this.queue = this.queue.filter((item) => item.id !== snippetId);
await idbKeyval.set(
`${articleId}-${snippetId}`,
{
...respond,
reaction: reactionProcessed,
createdTime: new Date().valueOf(),
},
dbTable.responseProcessed
);
try {
await idbKeyval.del(`${articleId}-${snippetId}`, dbTable.skipSnippet);
} catch (err) {}
if (this.responds.length && this.responds.length % 50 === 0) {
this.handleDownload.bind(this)();
}
}
trigger(prompt, checkInterval = this.checkInterval) {
return new Promise((resolve, reject) => {
const textEl = document.querySelector(this.INPUT_SELECTOR);
const submitEl = document.querySelector(this.SUBMIT_BTN_SELECTOR);
textEl.value = prompt;
textEl.dispatchEvent(new Event("input", { bubbles: true }));
setTimeout(() => {
submitEl.click();
let resCache = null;
let checkOutputCount = 0;
(async () => {
while (true) {
await this.sleep(checkInterval);
const result = Array.from(
document.querySelectorAll(this.RESPOND_SELECTOR)
);
const temp = result[result.length - 1];
if (!temp) {
if (checkOutputCount > 0) {
console.log("检查结果超时");
reject(null);
break;
}
checkOutputCount++;
continue;
}
if (resCache === temp.innerHTML) {
// console.log("匹配,resCache:", resCache);
const validateResult = await this.validate(resCache).catch(
(err) => {
reject(null);
return;
}
);
if (validateResult === true) {
resolve(resCache);
break;
} else if (validateResult === false) {
continue;
}
reject(null);
break;
}
resCache = temp.innerHTML;
console.log(`${checkInterval / 1000}s后再次检查结果`);
}
})();
}, 4000);
});
}
async validate(innerHTML) {
const buttons = document.querySelectorAll(
this.NORMAL_RESPOND_BTN_SELECTOR
);
const errorBtn = document.querySelectorAll(
this.ERROR_RESPOND_BTN_SELECTOR
);
const feedbackBtns = document.querySelectorAll('main .final-completion button[class*="final-completion"]')
const regenerateBtn = feedbackBtns[feedbackBtns.length - 1];
// 如果触发gpt-4 3小时25次限制
if (!regenerateBtn && !errorBtn[0] && innerHTML.includes("usage cap")) {
console.error("触发gpt-4 3小时25次限制,等待10min后重试");
await this.sleep(10 * 60 * 1000);
throw new Error("触发gpt-4 3小时25次限制");
}
// 如果openAI服务器报错未返回结果
if (errorBtn[0]) {
// && innerHTML.includes("wrong")) {
if (this.retrying) {
this.retrying = false;
return true;
}
errorBtn[0].click();
this.retrying = true;
return false;
}
// 如果输出结果未包含code标签
if (!innerHTML.includes("</code>")) {
if (this.retrying) {
this.retrying = false;
console.error("第二次还是未输出yaml结构");
throw new Error("未返回yaml结构");
}
console.error("未输出yaml结构,重试一次");
regenerateBtn.click();
this.retrying = true;
return false;
}
this.retrying = false;
// 如果还未完全输出
if (
buttons.length > 1 &&
!buttons[buttons.length - 1].innerText.includes("Regenerate")
) {
buttons[buttons.length - 1].click();
return false;
}
return true;
}
async main(sleepTime = 5000) {
let emptyCount = 0;
while (true) {
// {0: gpt-3.5, 1: gpt-4}
const modelNum = this.modelNum;
// const gpt4btn = document.querySelectorAll(
// "ul > li > button.cursor-pointer"
// )[modelNum];
// if (gpt4btn) {
// console.log(`当前模型为:${gpt4btn.innerText}`);
// gpt4btn.firstChild.click();
// } else {
// console.warn(`无法选择模型,2分钟后刷新`);
// await this.sleep(2 * 60 * 1000);
// locationReload();
// }
const currentModel = document.querySelector('main [aria-haspopup="menu"]')?.innerText;
const isGPT4 = currentModel.trim() === "ChatGPT 4"
await this.sleep(sleepTime / 2);
if (
modelNum === 1 && !isGPT4
) {
console.log("未切换到gpt-4模式, 5分钟后重试");
const maxTime = this._getLastRespondTime();
const diff = new Date().valueOf() - maxTime;
if (maxTime && diff > 1.5 * 60 * 60 * 1000) {
console.warn("超时未刷新, 5分钟后刷新页面");
await this.sleep(5 * 60 * 1000);
locationReload();
break;
}
this.report(
`触发gpt-4 3小时25次限制,上次运行时间:${new Date(
maxTime
).toLocaleString()}`
);
await this.sleep(5 * 60 * 1000);
const newChatBtn = document.querySelector(
this.NEW_CHART_BTN_SELECTOR
);
newChatBtn.click();
continue;
}
const task = this.getTask();
if (!task) {
if (emptyCount > 0) {
console.warn("连续两次未获取到任务,2分钟后刷新");
await this.sleep(2 * 60 * 1000);
locationReload();
break;
}
emptyCount++;
await this.sleep(5 * 60 * 1000);
continue;
}
const result = await task();
if (result) {
this.saveRespond(result);
emptyCount = 0;
} else {
if (emptyCount > 0) {
const task = this.queue[0];
const { article_id, id } = task;
console.warn(
`${article_id}-${id}连续两次未获取到任务值,2分钟后刷新`
);
await this.skipSnippetHandler(article_id, id);
await this.sleep(2 * 60 * 1000);
locationReload();
break;
}
emptyCount += 1;
}
console.log(`${sleepTime / 1000}s后将再次触发`);
const newChatBtn = document.querySelector(this.NEW_CHART_BTN_SELECTOR);
newChatBtn.click();
await this.sleep(sleepTime / 2);
this._updateDownloadBtnText();
}
}
}
function secondInterval() {
console.log("start secondInterval...");
const sleep = (duration) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, duration);
});
};
setInterval(async () => {
const responds = await idbKeyval.values(dbTable.responseProcessed);
const maxTime = Math.max.apply(
null,
responds
.map((item) => item.createdTime)
.filter((item) => item)
.concat([0])
);
const diff = new Date().valueOf() - maxTime;
console.log(`last updated at: ${maxTime}, diff is ${diff}`);
if (maxTime && diff > 30 * 60 * 1000) {
console.warn("超时未刷新, 2分钟后刷新页面");
await sleep(2 * 60 * 1000);
locationReload();
}
}, 10 * 60 * 1000);
}
function start() {
const ACCOUNT_NAME_SELECTOR = "nav > div:last-child > div:last-child";
const nameEl = document.querySelector(ACCOUNT_NAME_SELECTOR);
const name = nameEl && nameEl.innerText;
if (name) {
new GPT_ASK_LOOP(name);
secondInterval();
} else {
setTimeout(() => {
start();
}, 5000);
}
}
start();
})();