rednote-mcp
Version:
A friendly tool to help you access and interact with Xiaohongshu (RedNote) content through Model Context Protocol.
192 lines (191 loc) • 8.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthManager = void 0;
const playwright_1 = require("playwright");
const cookieManager_1 = require("./cookieManager");
const dotenv = __importStar(require("dotenv"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const logger_1 = __importDefault(require("../utils/logger"));
dotenv.config();
class AuthManager {
constructor(cookiePath) {
logger_1.default.info('Initializing AuthManager');
this.browser = null;
this.context = null;
this.page = null;
// Set default cookie path to ~/.mcp/rednote/cookies.json
if (!cookiePath) {
const homeDir = os.homedir();
const mcpDir = path.join(homeDir, '.mcp');
const rednoteDir = path.join(mcpDir, 'rednote');
// Create directories if they don't exist
if (!fs.existsSync(mcpDir)) {
logger_1.default.info(`Creating directory: ${mcpDir}`);
fs.mkdirSync(mcpDir);
}
if (!fs.existsSync(rednoteDir)) {
logger_1.default.info(`Creating directory: ${rednoteDir}`);
fs.mkdirSync(rednoteDir);
}
cookiePath = path.join(rednoteDir, 'cookies.json');
}
logger_1.default.info(`Using cookie path: ${cookiePath}`);
this.cookieManager = new cookieManager_1.CookieManager(cookiePath);
}
async getBrowser() {
logger_1.default.info('Launching browser');
// 始终创建新的浏览器实例,不复用之前的
this.browser = await playwright_1.chromium.launch({
headless: false,
});
return this.browser;
}
async getCookies() {
logger_1.default.info('Loading cookies');
return await this.cookieManager.loadCookies();
}
async login(options) {
const timeoutSeconds = options?.timeout || 10;
logger_1.default.info(`Starting login process with timeout: ${timeoutSeconds}s`);
const timeoutMs = timeoutSeconds * 1000;
this.browser = await playwright_1.chromium.launch({
headless: false,
timeout: timeoutMs,
});
if (!this.browser) {
logger_1.default.error('Failed to launch browser');
throw new Error('Failed to launch browser');
}
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
logger_1.default.info(`Login attempt ${retryCount + 1}/${maxRetries}`);
this.context = await this.browser.newContext();
this.page = await this.context.newPage();
// Load existing cookies if available
const cookies = await this.cookieManager.loadCookies();
if (cookies && cookies.length > 0) {
logger_1.default.info(`Loaded ${cookies.length} existing cookies`);
await this.context.addCookies(cookies);
}
// Navigate to explore page
logger_1.default.info('Navigating to explore page');
await this.page.goto('https://www.xiaohongshu.com/explore', {
waitUntil: 'domcontentloaded',
timeout: timeoutMs
});
// Check if already logged in
const userSidebar = await this.page.$('.user.side-bar-component .channel');
if (userSidebar) {
const isLoggedIn = await this.page.evaluate(() => {
const sidebarUser = document.querySelector('.user.side-bar-component .channel');
return sidebarUser?.textContent?.trim() === '我';
});
if (isLoggedIn) {
logger_1.default.info('Already logged in');
// Already logged in, save cookies and return
const newCookies = await this.context.cookies();
await this.cookieManager.saveCookies(newCookies);
return;
}
}
logger_1.default.info('Waiting for login dialog');
// Wait for login dialog if not logged in
await this.page.waitForSelector('.login-container', {
timeout: timeoutMs
});
// Wait for QR code image
logger_1.default.info('Waiting for QR code');
const qrCodeImage = await this.page.waitForSelector('.qrcode-img', {
timeout: timeoutMs
});
// Wait for user to complete login
logger_1.default.info('Waiting for user to complete login');
await this.page.waitForSelector('.user.side-bar-component .channel', {
timeout: timeoutMs * 6
});
// Verify the text content
const isLoggedIn = await this.page.evaluate(() => {
const sidebarUser = document.querySelector('.user.side-bar-component .channel');
return sidebarUser?.textContent?.trim() === '我';
});
if (!isLoggedIn) {
logger_1.default.error('Login verification failed');
throw new Error('Login verification failed');
}
logger_1.default.info('Login successful, saving cookies');
// Save cookies after successful login
const newCookies = await this.context.cookies();
await this.cookieManager.saveCookies(newCookies);
return;
}
catch (error) {
logger_1.default.error(`Login attempt ${retryCount + 1} failed:`, error);
// Clean up current session
if (this.page)
await this.page.close();
if (this.context)
await this.context.close();
retryCount++;
if (retryCount < maxRetries) {
logger_1.default.info(`Retrying login in 2 seconds (${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
else {
logger_1.default.error('Login failed after maximum retries');
throw new Error('Login failed after maximum retries');
}
}
}
}
async cleanup() {
logger_1.default.info('Cleaning up browser resources');
if (this.page)
await this.page.close();
if (this.context)
await this.context.close();
this.page = null;
this.context = null;
this.browser = null;
}
}
exports.AuthManager = AuthManager;