// ==UserScript==
// @name Notion AI
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 通过WebSocket自动填充提示词Notion AI并执行
// @author You
// @match https://www.notion.so/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 判断当前窗口是否是顶层窗口
if (window.top === window.self) {
// 是顶层窗口,执行你的主要代码
console.log("脚本在主页面运行");
// ... 你的所有代码都放在这里 ...
} else {
// 是在 iframe 中,不执行或执行其他逻辑
console.log("脚本在 iframe 中,已跳过");
return;
}
let ws = null;
let reconnectInterval = null;
const WS_URL = 'ws://localhost:7999'; // WebSocket端口
let currentRequestId = null;
let isProcessing = false;
// 日志函数
function log(message, data = null) {
console.log(`[Notion AI] ${message}`, data || '');
}
// 连接WebSocket
function connectWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) {
return;
}
log('正在连接WebSocket服务器...');
ws = new WebSocket(WS_URL);
ws.onopen = function() {
log('WebSocket连接成功');
if (reconnectInterval) {
clearInterval(reconnectInterval);
reconnectInterval = null;
}
// 发送注册消息
ws.send(JSON.stringify({
type: 'register',
clientId: generateClientId()
}));
};
ws.onmessage = async function(event) {
try {
const data = JSON.parse(event.data);
log(`收到消息:${data.type}`);
if (data.type === 'prompt' && data.content && !isProcessing) {
currentRequestId = data.requestId;
await processPrompt(data);
}
else if(data.type === 'refresh_page') {
log('收到页面刷新信号,即将刷新页面...');
// 等待1秒后刷新页面,给用户一个视觉提示的时间
setTimeout(() => {
window.location.reload();
}, 500);
}
else if(data.type === 'action'){
}
else{
sendFinishResponse('')
}
} catch (error) {
log('处理消息错误:', error);
isProcessing = false;
}
};
ws.onclose = function() {
log('WebSocket连接关闭');
ws = null;
startReconnect();
};
ws.onerror = function(error) {
log('WebSocket错误:', error);
};
}
// 自动重连
function startReconnect() {
if (!reconnectInterval) {
reconnectInterval = setInterval(() => {
log('尝试重新连接...');
connectWebSocket();
}, 5000);
}
}
// 生成客户端ID
function generateClientId() {
return 'client_' + Math.random().toString(36).substr(2, 9);
}
// 方法1: 直接点击元素(推荐用于 SPA)
function clickLogoLink() {
const newChatButton = document.querySelector('div[role="button"][aria-label="New chat"]');
if (newChatButton) {
simulateRealClick(newChatButton);
console.log('点击了 New chat 按钮');
return true;
}
return false;
}
// 处理提示词
async function processPrompt(data) {
const prompt = data.content;
const modelName = data.modelName;
log('开始处理提示词:',modelName, prompt.substr(0,50));
try {
let clickLogoSuccessed = clickLogoLink();
if(!clickLogoSuccessed){
// throw new Error('未点击新对话logo');
log('未点击新对话logo');
}
await new Promise(resolve => setTimeout(resolve, 1000));
// 查找textarea元素
let retry=0;
let textarea;
while(retry<3){
retry++;
textarea = document.querySelector('div[contenteditable="true"][role="textbox"][aria-label="Start typing to edit text"]');
if(!textarea){
log('获取失败,尝试重新获取:');
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (!textarea) {
throw new Error('未找到输入框');
}
// 使用真实鼠标点击聚焦
simulateRealClick(textarea);
await new Promise(resolve => setTimeout(resolve, 300));
// 清除原内容
textarea.innerHTML = '';
// 如果以上方法都失败,直接设置文本
if (!textarea.textContent) {
textarea.textContent = prompt;
log('使用直接设置内容完成');
}
// 移除占位符样式
textarea.style.webkitTextFillColor = '';
textarea.style.color = 'rgb(50, 48, 44)';
// 触发必要的事件序列
const events = ['focus', 'input', 'keydown', 'keypress', 'keyup', 'change'];
for (const eventType of events) {
const event = new Event(eventType, {
bubbles: true,
cancelable: true,
});
textarea.dispatchEvent(event);
await new Promise(resolve => setTimeout(resolve, 50));
}
// 再次使用真实鼠标点击确保输入已激活
simulateRealClick(textarea);
await new Promise(resolve => setTimeout(resolve, 300));
log('已设置提示词到输入框');
// 等待按钮可用
await waitForRunButton();
// 开始监听响应
interceptResponse(data);
// 点击运行按钮
await clickRunButton();
// clickLogoLink();
} catch (error) {
log('处理提示词错误:', error);
sendError(error.message);
isProcessing = false;
}
}
// 等待运行按钮可用
async function waitForRunButton(maxAttempts = 30) {
for (let i = 0; i < maxAttempts; i++) {
const button = document.querySelector('div[role="button"][aria-label="Submit AI message"]');
if (button) {
log('运行按钮已可用');
return true;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error('运行按钮未能可用');
}
// 点击运行按钮
async function clickRunButton() {
const button = document.querySelector('div[role="button"][aria-label="Submit AI message"]');
if (!button) {
throw new Error('运行按钮不可用');
}
log('点击运行按钮');
// 使用模拟真实鼠标点击来点击按钮
simulateRealClick(button);
await new Promise(resolve => setTimeout(resolve, 500));
return true;
}
// 拦截响应
function interceptResponse(requestBody) {
let responseIntercepted = false;
// 拦截 fetch
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [resource, config] = args;
const url = (typeof resource === 'string') ? resource : resource.url;
// log('拦截到 fetch 请求:', url); // 如果是目标请求
if (url.includes('/runInferenceTranscript')) {
log('拦截到 runInferenceTranscript fetch 请求:', url);
responseIntercepted = true;
try {
isProcessing = true;
// console.warn(args[1].body);
// 修改body
const body = JSON.parse(args[1].body);
body.transcript[0].value = {
"type": "markdown-chat",
"model": requestBody.modelName
}
args[1].body = JSON.stringify(body);
const response = await originalFetch.apply(this, args);
// Clone the response before reading from it
const responseClone = response.clone();
const reader = responseClone.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {value, done} = await reader.read();
if (done) {
log('Stream complete');
sendFinishResponse('');
isProcessing = false;
window.fetch = originalFetch;
break;
}
const chunk = decoder.decode(value, {stream: true});
// log('收到数据块:', chunk);
sendResponse(chunk);
}
return response;
} catch (error) {
log('Fetch 错误:', error);
window.fetch = originalFetch;
sendError(error.toString());
throw error;
}
}
// 非目标请求直接放行
return originalFetch.apply(this, args);
};
// 超时处理
setTimeout(() => {
if (!responseIntercepted && isProcessing) {
log('响应超时,恢复原始方法');
window.fetch = originalFetch;
sendError('响应超时');
}
}, 3000);
}
// 发送响应
function sendResponse(content) {
if (ws && ws.readyState === WebSocket.OPEN && currentRequestId) {
log('发送响应内容');
ws.send(JSON.stringify({
type: 'response',
requestId: currentRequestId,
content: content,
success: true
}));
}
}
function sendFinishResponse(content){
isProcessing = false;
if (ws && ws.readyState === WebSocket.OPEN && currentRequestId) {
log('发送结束消息');
ws.send(JSON.stringify({
type: 'finish',
requestId: currentRequestId,
content: content,
success: true
}));
currentRequestId = null;
}
}
// 发送错误
function sendError(error) {
isProcessing = false;
if (ws && ws.readyState === WebSocket.OPEN && currentRequestId) {
log('发送错误信息:', error);
ws.send(JSON.stringify({
type: 'response',
requestId: currentRequestId,
error: error,
success: false
}));
currentRequestId = null;
}
}
// 辅助函数:睡眠
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 模拟真实的鼠标点击
function simulateRealClick(element) {
if (!element) return false;
// 获取元素位置
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// 模拟鼠标移动到元素上
const moveEvent = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY
});
element.dispatchEvent(moveEvent);
// 模拟鼠标按下
const mouseDownEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0 // 左键
});
element.dispatchEvent(mouseDownEvent);
// 模拟鼠标松开
const mouseUpEvent = new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0 // 左键
});
element.dispatchEvent(mouseUpEvent);
// 模拟点击
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0 // 左键
});
element.dispatchEvent(clickEvent);
return true;
}
// 监控所有网络请求(调试用)
function monitorAllRequests() {
// 监控 XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
if (url.includes('google.com')) {
console.log(`[Network Monitor] XHR ${method} ${url}`);
}
return originalXHROpen.apply(this, [method, url, ...args]);
};
// 监控 fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
const [url] = args;
if (typeof url === 'string' && url.includes('google.com')) {
console.log(`[Network Monitor] Fetch ${url}`);
}
return originalFetch.apply(this, args);
};
}
// 初始化
function init() {
log('油猴脚本初始化');
// 启用网络监控(调试用,可注释掉)
if (window.location.href.includes('debug=true')) {
monitorAllRequests();
log('网络监控已启用');
}
connectWebSocket();
// 监听页面变化,确保元素存在
const observer = new MutationObserver(() => {
// log('页面元变换');
const textarea = document.querySelector('div[contenteditable="true"][role="textbox"][aria-label="Start typing to edit text"]');
const button = document.querySelector('div[role="button"][aria-label="Submit AI message"]');
if (textarea && button) {
log('页面元素已就绪');
}else{
if(textarea){
log('⚠️页面元素未找到 textarea');
}
if(button){
log('⚠️页面元素未找到 button');
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 页面加载完成后初始化
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
})();