Spaces:
Paused
Paused
| 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'; | |
| import { storageManager } from './utils/storage.js'; | |
| // 获取当前文件的目录路径 | |
| 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, | |
| enabled: true, // 新增enabled字段,默认启用 | |
| lastUsed: 0, // 记录上次使用时间戳 | |
| threadId: null // 新增threadId字段 | |
| }); | |
| 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; | |
| } | |
| // 尝试加载之前保存的数据(如Thread ID等) | |
| const savedData = storageManager.loadCookieData(); | |
| if (savedData) { | |
| this.cookieEntries = storageManager.mergeCookieData(this.cookieEntries, savedData); | |
| logger.info('已恢复保存的Cookie数据'); | |
| } | |
| // 保存当前数据 | |
| storageManager.saveCookieData(this.cookieEntries); | |
| 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 enabledEntries = this.cookieEntries.filter(entry => entry.valid && entry.enabled); | |
| if (enabledEntries.length === 0) { | |
| logger.warning('没有启用的有效cookie'); | |
| // 检查是否有有效但被禁用的cookie | |
| const disabledValidEntries = this.cookieEntries.filter(entry => entry.valid && !entry.enabled); | |
| if (disabledValidEntries.length > 0) { | |
| logger.warning(`发现 ${disabledValidEntries.length} 个有效但被禁用的cookie,自动启用第一个`); | |
| // 自动启用第一个有效的cookie | |
| disabledValidEntries[0].enabled = true; | |
| // 保存更新后的数据 | |
| storageManager.saveCookieData(this.cookieEntries); | |
| // 递归调用以返回启用的cookie | |
| return this.getNext(); | |
| } | |
| return null; | |
| } | |
| // 在启用的cookie中轮询 | |
| const entry = enabledEntries[this.currentIndex % enabledEntries.length]; | |
| // 更新索引,实现轮询 | |
| this.currentIndex = (this.currentIndex + 1) % enabledEntries.length; | |
| // 更新最后使用时间 | |
| entry.lastUsed = Date.now(); | |
| return { | |
| cookie: entry.cookie, | |
| spaceId: entry.spaceId, | |
| userId: entry.userId, | |
| threadId: entry.threadId // 返回threadId | |
| }; | |
| } | |
| /** | |
| * 标记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 && entry.enabled).length; | |
| } | |
| /** | |
| * 获取所有cookie的状态信息 | |
| * @returns {Array} - cookie状态数组 | |
| */ | |
| getStatus() { | |
| return this.cookieEntries.map((entry, index) => ({ | |
| index, | |
| userId: entry.userId, | |
| spaceId: entry.spaceId, | |
| valid: entry.valid, | |
| enabled: entry.enabled !== false, // 确保兼容旧数据 | |
| lastUsed: entry.lastUsed ? new Date(entry.lastUsed).toLocaleString() : 'never', | |
| threadId: entry.threadId, | |
| cookiePreview: this.getCookiePreview(entry.cookie) // 添加cookie预览 | |
| })); | |
| } | |
| /** | |
| * 获取cookie的预览(脱敏显示) | |
| * @param {string} cookie - 完整的cookie字符串 | |
| * @returns {string} - 脱敏后的cookie预览 | |
| */ | |
| getCookiePreview(cookie) { | |
| if (!cookie || cookie.length < 20) { | |
| return '***'; | |
| } | |
| // 显示前10个字符和后10个字符 | |
| return `${cookie.substring(0, 10)}...${cookie.substring(cookie.length - 10)}`; | |
| } | |
| /** | |
| * 设置指定用户的threadId | |
| * @param {string} userId - 用户ID | |
| * @param {string|null} threadId - Thread ID | |
| * @returns {boolean} - 是否设置成功 | |
| */ | |
| setThreadId(userId, threadId) { | |
| const entry = this.cookieEntries.find(e => e.userId === userId); | |
| if (entry) { | |
| entry.threadId = threadId; | |
| logger.info(`已为用户 ${userId} 设置Thread ID: ${threadId || '(null)'}`); | |
| // 保存更新后的数据 | |
| storageManager.saveCookieData(this.cookieEntries); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * 添加新的cookie | |
| * @param {string} cookieString - cookie字符串 | |
| * @param {string|null} threadId - 可选的Thread ID | |
| * @returns {Promise<Object>} - 添加结果 | |
| */ | |
| async addCookie(cookieString, threadId = null) { | |
| try { | |
| const result = await this.fetchNotionIds(cookieString); | |
| if (result.success) { | |
| // 检查是否已存在 | |
| const existing = this.cookieEntries.find(e => e.userId === result.userId); | |
| if (existing) { | |
| existing.cookie = cookieString; | |
| existing.valid = true; | |
| if (threadId !== undefined) { | |
| existing.threadId = threadId; | |
| } | |
| logger.info(`更新了现有cookie: ${result.userId}`); | |
| } else { | |
| this.cookieEntries.push({ | |
| cookie: cookieString, | |
| spaceId: result.spaceId, | |
| userId: result.userId, | |
| valid: true, | |
| enabled: true, | |
| lastUsed: 0, | |
| threadId: threadId | |
| }); | |
| logger.info(`添加了新cookie: ${result.userId}`); | |
| } | |
| // 保存更新后的数据 | |
| storageManager.saveCookieData(this.cookieEntries); | |
| return { success: true, userId: result.userId }; | |
| } else { | |
| return { success: false, error: result.error }; | |
| } | |
| } catch (error) { | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| /** | |
| * 删除指定用户的cookie | |
| * @param {string} userId - 用户ID | |
| * @returns {boolean} - 是否删除成功 | |
| */ | |
| deleteCookie(userId) { | |
| const index = this.cookieEntries.findIndex(e => e.userId === userId); | |
| if (index !== -1) { | |
| this.cookieEntries.splice(index, 1); | |
| logger.info(`已删除用户 ${userId} 的cookie`); | |
| // 重置当前索引 | |
| if (this.cookieEntries.length > 0 && this.currentIndex >= this.cookieEntries.length) { | |
| this.currentIndex = 0; | |
| } | |
| // 保存更新后的数据 | |
| storageManager.saveCookieData(this.cookieEntries); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * 切换cookie的启用状态 | |
| * @param {string} userId - 用户ID | |
| * @param {boolean} enabled - 是否启用 | |
| * @returns {boolean} - 是否操作成功 | |
| */ | |
| toggleCookie(userId, enabled) { | |
| const entry = this.cookieEntries.find(e => e.userId === userId); | |
| if (entry) { | |
| entry.enabled = enabled; | |
| logger.info(`已${enabled ? '启用' : '禁用'}用户 ${userId} 的cookie`); | |
| // 保存更新后的数据 | |
| storageManager.saveCookieData(this.cookieEntries); | |
| return true; | |
| } | |
| return false; | |
| } | |
| } | |
| export const cookieManager = new CookieManager(); | |