// ==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;
})();