// ==UserScript== // @name 小红书全量数据采集 (Source标识 + 服务器回显) // @namespace http://tampermonkey.net/ // @version 1.5 // @description 采集 InitialState 和 Feed 流,标识数据来源,并在气泡中显示服务器返回的具体消息。 // @author Gemini // @match https://www.xiaohongshu.com/* // @match https://edith.xiaohongshu.com/* // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect 192.168.2.114 // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/559126/%E5%B0%8F%E7%BA%A2%E4%B9%A6%E5%85%A8%E9%87%8F%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%20%28Source%E6%A0%87%E8%AF%86%20%2B%20%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%9B%9E%E6%98%BE%29.user.js // @updateURL https://update.greasyfork.icu/scripts/559126/%E5%B0%8F%E7%BA%A2%E4%B9%A6%E5%85%A8%E9%87%8F%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%20%28Source%E6%A0%87%E8%AF%86%20%2B%20%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%9B%9E%E6%98%BE%29.meta.js // ==/UserScript== (function() { 'use strict'; // ========================================================= // 配置区域 // ========================================================= // ⚠️ 请确保此IP和端口与你的Python服务器一致 const RECEIVE_SERVER_URL = 'http://192.168.2.114:8000/receive_feed'; // ⚠️ 红色警告:此API路径尚未经过官方文档验证,基于经验分析得出。 // 如果小红书更新接口版本(如改为 v2),此处必须手动更新。 const TARGET_API_PART = '/api/sns/web/v1/feed'; console.log('🛡️ 小红书采集 Hook (V1.5) 已注入'); // ========================================================= // 模块 0: UI 气泡提示系统 // ========================================================= const style = document.createElement('style'); style.innerHTML = ` #xhs-toast-container { position: fixed; top: 20px; right: 20px; z-index: 999999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; } .xhs-toast { min-width: 250px; max-width: 400px; padding: 12px 20px; border-radius: 8px; color: #fff; font-size: 14px; font-family: sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.15); opacity: 0; transform: translateX(20px); transition: all 0.3s ease; display: flex; align-items: center; word-break: break-all; } .xhs-toast.show { opacity: 1; transform: translateX(0); } .xhs-toast-success { background-color: #52c41a; } .xhs-toast-error { background-color: #ff4d4f; } .xhs-toast-info { background-color: #1890ff; } .xhs-toast-icon { margin-right: 8px; font-size: 16px; flex-shrink: 0; } `; (document.head || document.documentElement).appendChild(style); function showToast(message, type = 'info') { let container = document.getElementById('xhs-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'xhs-toast-container'; (document.body || document.documentElement).appendChild(container); } const toast = document.createElement('div'); toast.className = `xhs-toast xhs-toast-${type}`; const icons = { success: '✅', error: '❌', info: 'ℹ️' }; // 允许消息中包含 HTML (用于显示服务器返回的复杂信息) toast.innerHTML = `${icons[type]}${message}`; container.appendChild(toast); void toast.offsetWidth; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentElement) toast.parentElement.removeChild(toast); }, 300); }, 5000); // 5秒后消失 } // ========================================================= // 模块 1: 数据发送逻辑 (核心修改) // ========================================================= /** * 发送数据并处理服务器返回的消息 * @param {string} source - 数据来源标识 * @param {object} payload - 原始数据 */ function sendData(source, payload) { // 1. 构造数据包,增加 source 字段 const wrapper = { source: source, // <--- 新增字段 capture_url: location.href, timestamp: new Date().getTime(), payload: payload }; GM_xmlhttpRequest({ method: "POST", url: RECEIVE_SERVER_URL, headers: { "Content-Type": "application/json" }, data: JSON.stringify(wrapper), onload: function(res) { if (res.status === 200) { // 2. 解析服务器返回值 let serverMsg = 'OK'; try { const jsonRes = JSON.parse(res.responseText); // 优先显示服务器返回的 msg 或 message 字段 serverMsg = jsonRes.msg || jsonRes.message || JSON.stringify(jsonRes); } catch (e) { // 如果不是JSON,直接显示文本 (截取前100字防止过长) serverMsg = res.responseText.substring(0, 100); } console.log(`✅ [${source}] 上传成功:`, serverMsg); // 在气泡中显示服务器返回的内容 showToast(`上传成功
服务端: ${serverMsg}`, 'success'); } else { console.error(`❌ [${source}] 上传失败: ${res.status}`); showToast(`上传失败 (${res.status})`, 'error'); } }, onerror: function(err) { console.error(`❌ [${source}] 连接失败`, err); showToast(`无法连接服务器`, 'error'); } }); } // ========================================================= // 模块 2: 初始数据采集 // ========================================================= function captureInitialState() { let checkCount = 0; const timer = setInterval(() => { checkCount++; if (unsafeWindow.__INITIAL_STATE__) { clearInterval(timer); showToast('捕获到 Initial State', 'info'); try { const stateData = JSON.parse(JSON.stringify(unsafeWindow.__INITIAL_STATE__)); // 传递具体的 source 标识 sendData('window.__INITIAL_STATE__', stateData); } catch (e) { console.error('❌ 解析失败', e); } } else if (checkCount >= 50) { clearInterval(timer); } }, 100); } captureInitialState(); // ========================================================= // 模块 3: XHR 流 Hook // ========================================================= const globalObj = unsafeWindow; const OriginalXHR = globalObj.XMLHttpRequest; class ProxyXHR extends OriginalXHR { constructor() { super(); this._url = ''; } open(method, url, async, user, password) { this._url = url; return super.open(method, url, async, user, password); } send(body) { if (this._url && this._url.includes(TARGET_API_PART)) { this.addEventListener('readystatechange', () => { if (this.readyState === 4 && this.status === 200) { try { const originalResp = this.responseText; const jsonResp = JSON.parse(originalResp); // 传递具体的 source 标识 (即 API 路径) sendData('/api/sns/web/v1/feed', jsonResp); Object.defineProperty(this, 'responseText', { get: () => originalResp }); Object.defineProperty(this, 'response', { get: () => originalResp }); } catch (e) { console.error('❌ Hook Error:', e); } } }); } return super.send(body); } } globalObj.XMLHttpRequest = ProxyXHR; })();