Skip to content

Commit 5535809

Browse files
committed
refactor: Use to domcontentloaded for determining page load completion.
* Tidy codes.
1 parent 9322bde commit 5535809

File tree

5 files changed

+47
-116
lines changed

5 files changed

+47
-116
lines changed

src/auth/authManager.ts

Lines changed: 8 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import { Browser, BrowserContext, Page, chromium, Cookie } from 'playwright';
2-
import { CookieManager } from './cookieManager';
1+
import {Browser, BrowserContext, chromium, Cookie, Page} from 'playwright';
2+
import {CookieManager} from './cookieManager';
33
import * as dotenv from 'dotenv';
44
import * as fs from 'fs';
55
import * as path from 'path';
66
import * as os from 'os';
7-
import { exec } from 'child_process';
8-
import { promisify } from 'util';
97

108
dotenv.config();
119

12-
const execAsync = promisify(exec);
13-
1410
export class AuthManager {
1511
private browser: Browser | null;
1612
private context: BrowserContext | null;
@@ -27,15 +23,15 @@ export class AuthManager {
2723
const homeDir = os.homedir();
2824
const mcpDir = path.join(homeDir, '.mcp');
2925
const rednoteDir = path.join(mcpDir, 'rednote');
30-
26+
3127
// Create directories if they don't exist
3228
if (!fs.existsSync(mcpDir)) {
3329
fs.mkdirSync(mcpDir);
3430
}
3531
if (!fs.existsSync(rednoteDir)) {
3632
fs.mkdirSync(rednoteDir);
3733
}
38-
34+
3935
cookiePath = path.join(rednoteDir, 'cookies.json');
4036
}
4137

@@ -51,36 +47,12 @@ export class AuthManager {
5147
return this.browser;
5248
}
5349

54-
async getPage(): Promise<Page> {
55-
if (!this.page) {
56-
const browser = await this.getBrowser();
57-
this.page = await browser.newPage();
58-
59-
// Load existing cookies if available
60-
const cookies = await this.getCookies();
61-
if (cookies.length > 0) {
62-
await this.page.context().addCookies(cookies);
63-
}
64-
}
65-
return this.page;
66-
}
67-
6850
async getCookies(): Promise<Cookie[]> {
6951
return await this.cookieManager.loadCookies();
7052
}
7153

72-
private async handleSecurityVerification() {
73-
if (!this.page) return;
74-
75-
const securityUrl = '/web-login/captcha';
76-
if (this.page.url().includes(securityUrl)) {
77-
await this.page.waitForTimeout(2000);
78-
await this.page.reload({ waitUntil: 'networkidle' });
79-
}
80-
}
81-
8254
async login(): Promise<void> {
83-
this.browser = await chromium.launch({ headless: false });
55+
this.browser = await chromium.launch({headless: false});
8456
if (!this.browser) {
8557
throw new Error('Failed to launch browser');
8658
}
@@ -101,8 +73,8 @@ export class AuthManager {
10173

10274
// Navigate to explore page
10375
await this.page.goto('https://www.xiaohongshu.com/explore', {
104-
waitUntil: 'networkidle',
105-
timeout: 30000
76+
waitUntil: 'domcontentloaded',
77+
timeout: 10000
10678
});
10779

10880
// Check if already logged in
@@ -112,7 +84,7 @@ export class AuthManager {
11284
const sidebarUser = document.querySelector('.user.side-bar-component .channel');
11385
return sidebarUser?.textContent?.trim() === '我';
11486
});
115-
87+
11688
if (isLoggedIn) {
11789
// Already logged in, save cookies and return
11890
const newCookies = await this.context.cookies();
@@ -131,32 +103,6 @@ export class AuthManager {
131103
timeout: 10000
132104
});
133105

134-
// save image code
135-
// if (qrCodeImage) {
136-
// const qrCodeSrc = await qrCodeImage.getAttribute('src');
137-
// if (qrCodeSrc) {
138-
// // Save QR code image
139-
// const qrCodeBuffer = Buffer.from(qrCodeSrc.split(',')[1], 'base64');
140-
// const tempDir = process.env.TEMP || process.env.TMP || '/tmp';
141-
// const qrCodePath = path.join(tempDir, 'xhs-login-qrcode.png');
142-
//
143-
// await fs.promises.writeFile(qrCodePath, qrCodeBuffer);
144-
//
145-
// // Open QR code image with system default viewer
146-
// try {
147-
// if (process.platform === 'darwin') {
148-
// await execAsync(`open ${qrCodePath}`);
149-
// } else if (process.platform === 'win32') {
150-
// await execAsync(`start ${qrCodePath}`);
151-
// } else if (process.platform === 'linux') {
152-
// await execAsync(`xdg-open ${qrCodePath}`);
153-
// }
154-
// } catch (error) {
155-
// // Silently handle error
156-
// }
157-
// }
158-
// }
159-
160106
// Wait for user to complete login
161107
await this.page.waitForSelector('.user.side-bar-component .channel', {
162108
timeout: 60000
@@ -191,20 +137,6 @@ export class AuthManager {
191137
}
192138
}
193139

194-
async checkLoginStatus(): Promise<boolean> {
195-
const page = await this.getPage();
196-
await page.goto('https://www.xiaohongshu.com');
197-
198-
// Check if login button exists
199-
const loginButton = await page.$('button[data-testid="login-button"], button:has-text("登录"), .login-button');
200-
return !loginButton;
201-
}
202-
203-
async isAuthenticated(): Promise<boolean> {
204-
const cookies = await this.cookieManager.loadCookies();
205-
return cookies.length > 0;
206-
}
207-
208140
async cleanup(): Promise<void> {
209141
if (this.page) await this.page.close();
210142
if (this.context) await this.context.close();

src/auth/cookieManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs';
22
import path from 'path';
3-
import { Cookie } from 'playwright';
3+
import {Cookie} from 'playwright';
44

55
export class CookieManager {
66
private readonly cookiePath: string;
@@ -12,7 +12,7 @@ export class CookieManager {
1212
async saveCookies(cookies: Cookie[]): Promise<void> {
1313
const dir = path.dirname(this.cookiePath);
1414
if (!fs.existsSync(dir)) {
15-
fs.mkdirSync(dir, { recursive: true });
15+
fs.mkdirSync(dir, {recursive: true});
1616
}
1717
await fs.promises.writeFile(this.cookiePath, JSON.stringify(cookies, null, 2));
1818
}

src/cli.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
#!/usr/bin/env node
22

3-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5-
import { z } from "zod";
6-
import { AuthManager } from './auth/authManager';
7-
import { RedNoteTools } from './tools/rednoteTools';
3+
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
4+
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
5+
import {z} from "zod";
6+
import {AuthManager} from './auth/authManager';
7+
import {RedNoteTools} from './tools/rednoteTools';
88

99
const tools = new RedNoteTools();
1010

11+
const name = "rednote";
12+
const description = "A friendly tool to help you access and interact with Xiaohongshu (RedNote) content through Model Context Protocol.";
13+
const version = "0.1.6";
14+
1115
// Create server instance
1216
const server = new McpServer({
13-
name: "rednote",
14-
version: "0.1.0",
17+
name,
18+
version,
1519
protocolVersion: "2024-11-05",
1620
capabilities: {
1721
tools: true,
@@ -26,7 +30,7 @@ const server = new McpServer({
2630
server.tool("search_notes", "根据关键词搜索笔记", {
2731
keywords: z.string().describe("搜索关键词"),
2832
limit: z.number().optional().describe("返回结果数量限制"),
29-
}, async ({ keywords, limit = 10 }: { keywords: string; limit?: number }) => {
33+
}, async ({keywords, limit = 10}: { keywords: string; limit?: number }) => {
3034
const notes = await tools.searchNotes(keywords, limit);
3135
return {
3236
content: notes.map(note => ({
@@ -38,7 +42,7 @@ server.tool("search_notes", "根据关键词搜索笔记", {
3842

3943
server.tool("get_note_content", "获取笔记内容", {
4044
url: z.string().describe("笔记 URL"),
41-
}, async ({ url }: { url: string }) => {
45+
}, async ({url}: { url: string }) => {
4246
const note = await tools.getNoteContent(url);
4347
return {
4448
content: [{
@@ -50,7 +54,7 @@ server.tool("get_note_content", "获取笔记内容", {
5054

5155
server.tool("get_note_comments", "获取笔记评论", {
5256
url: z.string().describe("笔记 URL"),
53-
}, async ({ url }: { url: string }) => {
57+
}, async ({url}: { url: string }) => {
5458
const comments = await tools.getNoteComments(url);
5559
return {
5660
content: comments.map(comment => ({
@@ -97,14 +101,13 @@ if (process.argv.includes('--stdio')) {
97101
process.exit(1);
98102
});
99103
} else {
100-
// 设置命令行程序
101-
const { Command } = require('commander');
104+
const {Command} = require('commander');
102105
const program = new Command();
103106

104107
program
105-
.name('rednote-mcp')
106-
.description('RedNote MCP implementation for Model Context Protocol')
107-
.version('0.1.0');
108+
.name(name)
109+
.description(description)
110+
.version(version);
108111

109112
program
110113
.command('init')

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import './cli';
1+
import './cli';

src/tools/rednoteTools.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { AuthManager } from '../auth/authManager';
2-
import { Browser, Page } from 'playwright';
1+
import {AuthManager} from '../auth/authManager';
2+
import {Browser, Page} from 'playwright';
33

44
interface Note {
55
title: string;
@@ -27,17 +27,6 @@ export class RedNoteTools {
2727
this.authManager = new AuthManager();
2828
}
2929

30-
/**
31-
* Wait for a random duration between min and max seconds
32-
* @param min Minimum seconds to wait
33-
* @param max Maximum seconds to wait
34-
*/
35-
private async randomDelay(min: number, max: number): Promise<void> {
36-
if (!this.page) throw new Error('Page not initialized');
37-
const ms = Math.floor(Math.random() * (max - min) * 1000) + min * 1000;
38-
await this.page.waitForTimeout(ms);
39-
}
40-
4130
async initialize(): Promise<void> {
4231
if (!this.browser) {
4332
this.browser = await this.authManager.getBrowser();
@@ -99,7 +88,7 @@ export class RedNoteTools {
9988
await this.page.waitForSelector('#noteContainer', {
10089
timeout: 30000
10190
});
102-
91+
10392
await this.randomDelay(0.5, 1.5);
10493

10594
// Extract note content
@@ -193,14 +182,14 @@ export class RedNoteTools {
193182
await this.page.waitForSelector('main article');
194183

195184
// Extract note content
196-
const note = await this.page.evaluate(() => {
185+
return await this.page.evaluate(() => {
197186
// Get main article content
198187
const article = document.querySelector('main article');
199188
if (!article) throw new Error('Article not found');
200189

201190
// Get title from h1 or first text block
202191
const title = article.querySelector('h1')?.textContent?.trim() ||
203-
article.querySelector('.title')?.textContent?.trim() || '';
192+
article.querySelector('.title')?.textContent?.trim() || '';
204193

205194
// Get content from article text
206195
const contentBlocks = Array.from(article.querySelectorAll('p, .content'));
@@ -227,8 +216,6 @@ export class RedNoteTools {
227216
comments
228217
};
229218
});
230-
231-
return note;
232219
} finally {
233220
await this.cleanup();
234221
}
@@ -245,7 +232,7 @@ export class RedNoteTools {
245232
await this.page.waitForSelector('[role="dialog"] [role="list"]');
246233

247234
// Extract comments
248-
const comments = await this.page.evaluate(() => {
235+
return await this.page.evaluate(() => {
249236
const items = document.querySelectorAll('[role="dialog"] [role="list"] [role="listitem"]');
250237
const results: Comment[] = [];
251238

@@ -255,15 +242,24 @@ export class RedNoteTools {
255242
const likes = parseInt(item.querySelector('[data-testid="likes-count"]')?.textContent || '0');
256243
const time = item.querySelector('time')?.textContent?.trim() || '';
257244

258-
results.push({ author, content, likes, time });
245+
results.push({author, content, likes, time});
259246
});
260247

261248
return results;
262249
});
263-
264-
return comments;
265250
} finally {
266251
await this.cleanup();
267252
}
268253
}
254+
255+
/**
256+
* Wait for a random duration between min and max seconds
257+
* @param min Minimum seconds to wait
258+
* @param max Maximum seconds to wait
259+
*/
260+
private async randomDelay(min: number, max: number): Promise<void> {
261+
if (!this.page) throw new Error('Page not initialized');
262+
const ms = Math.floor(Math.random() * (max - min) * 1000) + min * 1000;
263+
await this.page.waitForTimeout(ms);
264+
}
269265
}

0 commit comments

Comments
 (0)