Spaces:
Sleeping
Sleeping
import { JSDOM } from 'jsdom'; | |
import fetch from 'node-fetch'; | |
import fs from 'fs'; | |
import path from 'path'; | |
import { fileURLToPath } from 'url'; | |
import { dirname } from 'path'; | |
// 获取当前文件的目录路径 | |
const __filename = fileURLToPath(import.meta.url); | |
const __dirname = dirname(__filename); | |
// 日志配置 | |
const logger = { | |
info: (message) => console.log(`\x1b[34m[info] ${message}\x1b[0m`), | |
error: (message) => console.error(`\x1b[31m[error] ${message}\x1b[0m`), | |
warning: (message) => console.warn(`\x1b[33m[warn] ${message}\x1b[0m`), | |
success: (message) => console.log(`\x1b[32m[success] ${message}\x1b[0m`), | |
}; | |
class CookieManager { | |
constructor() { | |
this.cookieEntries = []; // 存储cookie及其对应的ID | |
this.currentIndex = 0; | |
this.initialized = false; | |
this.maxRetries = 3; // 最大重试次数 | |
this.proxyUrl = process.env.PROXY_URL || ""; | |
} | |
/** | |
* 从文件加载cookie | |
* @param {string} filePath - cookie文件路径 | |
* @returns {Promise<boolean>} - 是否加载成功 | |
*/ | |
async loadFromFile(filePath) { | |
try { | |
// 确保文件路径是绝对路径 | |
const absolutePath = path.isAbsolute(filePath) | |
? filePath | |
: path.join(dirname(__dirname), filePath); | |
logger.info(`从文件加载cookie: ${absolutePath}`); | |
// 检查文件是否存在 | |
if (!fs.existsSync(absolutePath)) { | |
logger.error(`Cookie文件不存在: ${absolutePath}`); | |
return false; | |
} | |
// 读取文件内容 | |
const fileContent = fs.readFileSync(absolutePath, 'utf8'); | |
// 根据文件扩展名处理不同格式 | |
const ext = path.extname(absolutePath).toLowerCase(); | |
let cookieArray = []; | |
if (ext === '.json') { | |
// JSON格式 | |
try { | |
const jsonData = JSON.parse(fileContent); | |
if (Array.isArray(jsonData)) { | |
cookieArray = jsonData; | |
} else if (jsonData.cookies && Array.isArray(jsonData.cookies)) { | |
cookieArray = jsonData.cookies; | |
} else { | |
logger.error('JSON文件格式错误,应为cookie数组或包含cookies数组的对象'); | |
return false; | |
} | |
} catch (error) { | |
logger.error(`解析JSON文件失败: ${error.message}`); | |
return false; | |
} | |
} else { | |
// 文本格式,每行一个cookie | |
cookieArray = fileContent | |
.split('\n') | |
.map(line => line.trim()) | |
.filter(line => line && !line.startsWith('#')); | |
} | |
logger.info(`从文件中读取了 ${cookieArray.length} 个cookie`); | |
// 初始化cookie | |
return await this.initialize(cookieArray.join('|')); | |
} catch (error) { | |
logger.error(`从文件加载cookie失败: ${error.message}`); | |
return false; | |
} | |
} | |
/** | |
* 初始化cookie管理器 | |
* @param {string} cookiesString - 以"|"分隔的cookie字符串 | |
* @returns {Promise<boolean>} - 是否初始化成功 | |
*/ | |
async initialize(cookiesString) { | |
if (!cookiesString) { | |
logger.error('未提供cookie字符串'); | |
return false; | |
} | |
// 分割cookie字符串 | |
const cookieArray = cookiesString.split('|').map(c => c.trim()).filter(c => c); | |
if (cookieArray.length === 0) { | |
logger.error('没有有效的cookie'); | |
return false; | |
} | |
logger.info(`发现 ${cookieArray.length} 个cookie,开始获取对应的ID信息...`); | |
// 清空现有条目 | |
this.cookieEntries = []; | |
// 为每个cookie获取ID | |
for (let i = 0; i < cookieArray.length; i++) { | |
const cookie = cookieArray[i]; | |
logger.info(`正在处理第 ${i+1}/${cookieArray.length} 个cookie...`); | |
const result = await this.fetchNotionIds(cookie); | |
if (result.success) { | |
this.cookieEntries.push({ | |
cookie, | |
spaceId: result.spaceId, | |
userId: result.userId, | |
valid: true, | |
lastUsed: 0 // 记录上次使用时间戳 | |
}); | |
logger.success(`第 ${i+1} 个cookie验证成功`); | |
} else { | |
if (result.status === 401) { | |
logger.error(`第 ${i+1} 个cookie无效(401未授权),已跳过`); | |
} else { | |
logger.warning(`第 ${i+1} 个cookie验证失败: ${result.error},已跳过`); | |
} | |
} | |
} | |
// 检查是否有有效的cookie | |
if (this.cookieEntries.length === 0) { | |
logger.error('没有有效的cookie,初始化失败'); | |
return false; | |
} | |
logger.success(`成功初始化 ${this.cookieEntries.length}/${cookieArray.length} 个cookie`); | |
this.initialized = true; | |
this.currentIndex = 0; | |
return true; | |
} | |
/** | |
* 保存cookie到文件 | |
* @param {string} filePath - 保存路径 | |
* @param {boolean} onlyValid - 是否只保存有效的cookie | |
* @returns {boolean} - 是否保存成功 | |
*/ | |
saveToFile(filePath, onlyValid = true) { | |
try { | |
// 确保文件路径是绝对路径 | |
const absolutePath = path.isAbsolute(filePath) | |
? filePath | |
: path.join(dirname(__dirname), filePath); | |
// 获取要保存的cookie | |
const cookiesToSave = onlyValid | |
? this.cookieEntries.filter(entry => entry.valid).map(entry => entry.cookie) | |
: this.cookieEntries.map(entry => entry.cookie); | |
// 根据文件扩展名选择保存格式 | |
const ext = path.extname(absolutePath).toLowerCase(); | |
if (ext === '.json') { | |
// 保存为JSON格式 | |
const jsonData = { | |
cookies: cookiesToSave, | |
updatedAt: new Date().toISOString(), | |
count: cookiesToSave.length | |
}; | |
fs.writeFileSync(absolutePath, JSON.stringify(jsonData, null, 2), 'utf8'); | |
} else { | |
// 保存为文本格式,每行一个cookie | |
const content = cookiesToSave.join('\n'); | |
fs.writeFileSync(absolutePath, content, 'utf8'); | |
} | |
logger.success(`已将 ${cookiesToSave.length} 个cookie保存到文件: ${absolutePath}`); | |
return true; | |
} catch (error) { | |
logger.error(`保存cookie到文件失败: ${error.message}`); | |
return false; | |
} | |
} | |
/** | |
* 获取Notion的空间ID和用户ID | |
* @param {string} cookie - Notion cookie | |
* @returns {Promise<Object>} - 包含ID信息的对象 | |
*/ | |
async fetchNotionIds(cookie, retryCount = 0) { | |
if (!cookie) { | |
return { success: false, error: '未提供cookie' }; | |
} | |
try { | |
// 创建JSDOM实例模拟浏览器环境 | |
const dom = new JSDOM("", { | |
url: "https://www.notion.so", | |
referrer: "https://www.notion.so/", | |
contentType: "text/html", | |
includeNodeLocations: true, | |
storageQuota: 10000000, | |
pretendToBeVisual: true, | |
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" | |
}); | |
// 设置全局对象 | |
const { window } = dom; | |
// 安全地设置全局对象 | |
if (!global.window) global.window = window; | |
if (!global.document) global.document = window.document; | |
// 设置navigator | |
if (!global.navigator) { | |
try { | |
Object.defineProperty(global, 'navigator', { | |
value: window.navigator, | |
writable: true, | |
configurable: true | |
}); | |
} catch (navError) { | |
logger.warning(`无法设置navigator: ${navError.message},继续执行`); | |
} | |
} | |
// 设置cookie | |
document.cookie = cookie; | |
// 创建fetch选项 | |
const fetchOptions = { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'accept': '*/*', | |
'accept-language': 'en-US,en;q=0.9', | |
'notion-audit-log-platform': 'web', | |
'notion-client-version': '23.13.0.3686', | |
'origin': 'https://www.notion.so', | |
'referer': 'https://www.notion.so/', | |
'user-agent': window.navigator.userAgent, | |
'Cookie': cookie | |
}, | |
body: JSON.stringify({}), | |
}; | |
// 发送请求 | |
const response = await fetch("https://www.notion.so/api/v3/getSpaces", fetchOptions); | |
// 检查响应状态 | |
if (response.status === 401) { | |
return { success: false, status: 401, error: '未授权,cookie无效' }; | |
} | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
// 提取用户ID | |
const userIdKey = Object.keys(data)[0]; | |
if (!userIdKey) { | |
throw new Error('无法从响应中提取用户ID'); | |
} | |
const userId = userIdKey; | |
// 提取空间ID | |
const userRoot = data[userIdKey]?.user_root?.[userIdKey]; | |
const spaceViewPointers = userRoot?.value?.value?.space_view_pointers; | |
if (!spaceViewPointers || !Array.isArray(spaceViewPointers) || spaceViewPointers.length === 0) { | |
throw new Error('在响应中找不到space_view_pointers或spaceId'); | |
} | |
const spaceId = spaceViewPointers[0].spaceId; | |
if (!spaceId) { | |
throw new Error('无法从space_view_pointers中提取spaceId'); | |
} | |
// 清理全局对象 | |
this.cleanupGlobalObjects(); | |
return { | |
success: true, | |
userId, | |
spaceId | |
}; | |
} catch (error) { | |
// 清理全局对象 | |
this.cleanupGlobalObjects(); | |
// 重试逻辑 | |
if (retryCount < this.maxRetries && error.message !== '未授权,cookie无效') { | |
logger.warning(`获取Notion ID失败,正在重试 (${retryCount + 1}/${this.maxRetries}): ${error.message}`); | |
return await this.fetchNotionIds(cookie, retryCount + 1); | |
} | |
return { | |
success: false, | |
error: error.message | |
}; | |
} | |
} | |
/** | |
* 清理全局对象 | |
*/ | |
cleanupGlobalObjects() { | |
try { | |
if (global.window) delete global.window; | |
if (global.document) delete global.document; | |
// 安全地删除navigator | |
if (global.navigator) { | |
try { | |
delete global.navigator; | |
} catch (navError) { | |
// 如果无法删除,尝试将其设置为undefined | |
try { | |
Object.defineProperty(global, 'navigator', { | |
value: undefined, | |
writable: true, | |
configurable: true | |
}); | |
} catch (defineError) { | |
logger.warning(`无法清理navigator: ${defineError.message}`); | |
} | |
} | |
} | |
} catch (cleanupError) { | |
logger.warning(`清理全局对象时出错: ${cleanupError.message}`); | |
} | |
} | |
/** | |
* 获取下一个可用的cookie及其ID | |
* @returns {Object|null} - cookie及其对应的ID,如果没有可用cookie则返回null | |
*/ | |
getNext() { | |
if (!this.initialized || this.cookieEntries.length === 0) { | |
return null; | |
} | |
// 轮询选择下一个cookie | |
const entry = this.cookieEntries[this.currentIndex]; | |
// 更新索引,实现轮询 | |
this.currentIndex = (this.currentIndex + 1) % this.cookieEntries.length; | |
// 更新最后使用时间 | |
entry.lastUsed = Date.now(); | |
return { | |
cookie: entry.cookie, | |
spaceId: entry.spaceId, | |
userId: entry.userId | |
}; | |
} | |
/** | |
* 标记cookie为无效 | |
* @param {string} userId - 用户ID | |
*/ | |
markAsInvalid(userId) { | |
const index = this.cookieEntries.findIndex(entry => entry.userId === userId); | |
if (index !== -1) { | |
this.cookieEntries[index].valid = false; | |
logger.warning(`已将用户ID为 ${userId} 的cookie标记为无效`); | |
// 过滤掉所有无效的cookie | |
this.cookieEntries = this.cookieEntries.filter(entry => entry.valid); | |
// 重置当前索引 | |
if (this.cookieEntries.length > 0) { | |
this.currentIndex = 0; | |
} | |
} | |
} | |
/** | |
* 获取有效cookie的数量 | |
* @returns {number} - 有效cookie的数量 | |
*/ | |
getValidCount() { | |
return this.cookieEntries.filter(entry => entry.valid).length; | |
} | |
/** | |
* 获取所有cookie的状态信息 | |
* @returns {Array} - cookie状态数组 | |
*/ | |
getStatus() { | |
return this.cookieEntries.map((entry, index) => ({ | |
index, | |
userId: entry.userId.substring(0, 8) + '...', | |
spaceId: entry.spaceId.substring(0, 8) + '...', | |
valid: entry.valid, | |
lastUsed: entry.lastUsed ? new Date(entry.lastUsed).toLocaleString() : 'never' | |
})); | |
} | |
} | |
export const cookieManager = new CookieManager(); |