Skip to main content
Glama
delete.service.ts18 kB
/** * Delete service for XHS MCP Server * Handles note deletion operations */ import type { Config, XHSResponse } from '../../shared/types'; import { BaseService } from '../../shared/base.service'; import { logger } from '../../shared/logger'; import { sleep } from '../../shared/utils'; import { DeleteError, NotLoggedInError } from '../../shared/errors'; import { Page } from 'puppeteer'; import { COMMON_BUTTON_SELECTORS, COMMON_MODAL_SELECTORS } from '../../shared/selectors'; export interface DeleteResult extends XHSResponse<null> { readonly noteId?: string; readonly title?: string; readonly deletedAt: number; } export interface DeleteOptions { noteId?: string; lastPublished?: boolean; browserPath?: string; } /** * CSS selectors for delete operations - updated for current XiaoHongShu interface */ const DELETE_SELECTORS = { NOTE_ITEM: [ 'div.note', '.note-item', '[class*="note"]', '[data-impression]', '.creator-note-item', '.note-card', '.content-item' ], DELETE_BUTTON: [ '.control.data-del', 'span.control.data-del', ...COMMON_BUTTON_SELECTORS.DELETE ], CONFIRM_BUTTON: COMMON_BUTTON_SELECTORS.CONFIRM, CANCEL_BUTTON: COMMON_BUTTON_SELECTORS.CANCEL, MORE_OPTIONS: COMMON_BUTTON_SELECTORS.MORE_OPTIONS, DROPDOWN_MENU: COMMON_MODAL_SELECTORS.DROPDOWN_MENU, MODAL_CONFIRM: COMMON_MODAL_SELECTORS.CONFIRM, MODAL_CANCEL: COMMON_MODAL_SELECTORS.CANCEL, } as const; export class DeleteService extends BaseService { constructor(config: Config) { super(config); } /** * Delete a specific note by ID * @param noteId - The ID of the note to delete * @param browserPath - Optional custom browser path * @returns Promise<DeleteResult> - Delete operation result */ async deleteNote(noteId: string, browserPath?: string): Promise<DeleteResult> { this.validateDeleteParams(noteId); const page = await this.getBrowserManager().createPage(true, browserPath, true); try { // Navigate to creator center note manager await this.navigateToCreatorCenter(page); await this.verifyUserAuthentication(page); // Find and delete the specific note const result = await this.findAndDeleteNote(page, noteId); return { success: true, data: null, noteId: result.noteId, title: result.title, deletedAt: Date.now(), message: `Successfully deleted note "${result.title}" (ID: ${result.noteId})`, operation: 'deleteNote', } as unknown as DeleteResult; } catch (error) { logger.error(`Failed to delete note ${noteId}: ${error}`); return { success: false, data: null, error: error instanceof Error ? error.message : String(error), message: `Failed to delete note: ${error instanceof Error ? error.message : String(error)}`, deletedAt: Date.now(), operation: 'deleteNote', } as unknown as DeleteResult; } finally { await page.close(); } } /** * Delete the last published note * @param browserPath - Optional custom browser path * @returns Promise<DeleteResult> - Delete operation result */ async deleteLastPublishedNote(browserPath?: string): Promise<DeleteResult> { const page = await this.getBrowserManager().createPage(true, browserPath, true); try { // Navigate to creator center note manager await this.navigateToCreatorCenter(page); await this.verifyUserAuthentication(page); // Find and delete the last published note const result = await this.findAndDeleteLastNote(page); return { success: true, data: null, noteId: result.noteId, title: result.title, deletedAt: Date.now(), message: `Successfully deleted last published note "${result.title}" (ID: ${result.noteId})`, operation: 'deleteLastPublishedNote', } as unknown as DeleteResult; } catch (error) { logger.error(`Failed to delete last published note: ${error}`); return { success: false, data: null, error: error instanceof Error ? error.message : String(error), message: `Failed to delete last published note: ${error instanceof Error ? error.message : String(error)}`, deletedAt: Date.now(), operation: 'deleteLastPublishedNote', } as unknown as DeleteResult; } finally { await page.close(); } } /** * Validate delete parameters */ private validateDeleteParams(noteId: string): void { if (!noteId || noteId.trim().length === 0) { throw new DeleteError('Note ID is required', { noteId }); } } /** * Navigate to creator center note manager */ private async navigateToCreatorCenter(page: Page): Promise<void> { try { const creatorCenterUrl = 'https://creator.xiaohongshu.com/new/note-manager?source=official'; await this.getBrowserManager().navigateWithRetry(page, creatorCenterUrl); await sleep(5000); // Wait longer for page to load completely // Debug: Log page content to understand the structure const pageContent = await page.evaluate(() => { const noteElements = document.querySelectorAll('div.note, [class*="note"], [data-impression]'); const allButtons = document.querySelectorAll('button, [role="button"], .btn, [class*="button"]'); const allLinks = document.querySelectorAll('a[href*="/explore/"]'); return { totalNoteElements: noteElements.length, noteElementClasses: Array.from(noteElements).map(el => el.className), totalButtons: allButtons.length, buttonTexts: Array.from(allButtons).slice(0, 10).map(btn => btn.textContent?.trim()).filter(Boolean), buttonClasses: Array.from(allButtons).slice(0, 10).map(btn => btn.className), exploreLinks: Array.from(allLinks).map(link => link.getAttribute('href')), pageTitle: document.title, currentUrl: window.location.href }; }); logger.info(`Page loaded: ${JSON.stringify(pageContent, null, 2)}`); } catch (error) { throw new DeleteError( 'Failed to navigate to creator center', { url: 'https://creator.xiaohongshu.com/new/note-manager?source=official' }, error instanceof Error ? error : new Error(String(error)) ); } } /** * Verify user is authenticated */ private async verifyUserAuthentication(page: Page): Promise<void> { try { // Check for login elements on the current page const loginElements = await page.$$(this.getConfig().xhs.loginOkSelector); // Also check for creator center specific elements const creatorElements = await page.$$( '[class*="user"], [class*="profile"], [class*="avatar"]' ); if (loginElements.length === 0 && creatorElements.length === 0) { // Check if we're on a login page const currentUrl = page.url(); if (currentUrl.includes('login') || currentUrl.includes('signin')) { throw new NotLoggedInError('User not logged in', { operation: 'deleteNote', url: currentUrl, }); } // For creator center, check if we can see note management elements const noteElements = await page.$$('div.note'); if (noteElements.length === 0) { throw new NotLoggedInError('User not logged in or no notes found', { operation: 'deleteNote', url: currentUrl, }); } } } catch (error) { if (error instanceof NotLoggedInError) { throw error; } throw new DeleteError( 'Failed to verify authentication', { operation: 'verifyAuth' }, error instanceof Error ? error : new Error(String(error)) ); } } /** * Find and delete a specific note by ID */ private async findAndDeleteNote(page: Page, noteId: string): Promise<{ noteId: string; title: string }> { try { const result = await page.evaluate( (selectors: typeof DELETE_SELECTORS, targetNoteId: string) => { // Try each note item selector let noteElements: Element[] = []; for (const selector of selectors.NOTE_ITEM) { const elements = Array.from(document.querySelectorAll(selector)); if (elements.length > 0) { noteElements = elements; break; } } if (noteElements.length === 0) { return { noteId: targetNoteId, title: '', found: false, error: 'No note elements found on page' }; } for (const noteElement of noteElements) { // Check if this is the note we want to delete const impressionData = noteElement.getAttribute('data-impression'); let currentNoteId = ''; if (impressionData) { try { const parsed = JSON.parse(impressionData); currentNoteId = parsed?.noteTarget?.value?.noteId || ''; } catch (e) { // Ignore parsing errors } } // Also try to find note ID in other attributes or data if (!currentNoteId) { const linkElement = noteElement.querySelector('a[href*="/explore/"], a[href*="/note/"]'); if (linkElement) { const href = linkElement.getAttribute('href') || ''; const match = href.match(/\/explore\/([a-zA-Z0-9]+)/); if (match) { currentNoteId = match[1]; } } } if (currentNoteId === targetNoteId) { // Found the target note, try to delete it const titleElement = noteElement.querySelector('[class*="title"], [class*="name"]'); const title = titleElement?.textContent?.trim() || 'Unknown'; // Look for delete button or more options button let deleteButton: Element | null = null; // Try each delete button selector for (const selector of selectors.DELETE_BUTTON) { deleteButton = noteElement.querySelector(selector); if (deleteButton) { break; } } if (!deleteButton) { // Try to find more options button first let moreButton: Element | null = null; for (const selector of selectors.MORE_OPTIONS) { moreButton = noteElement.querySelector(selector); if (moreButton) { break; } } if (moreButton) { (moreButton as HTMLElement).click(); // Wait for dropdown to appear and look for delete option setTimeout(() => { for (const selector of selectors.DROPDOWN_MENU) { const dropdown = document.querySelector(selector); if (dropdown) { for (const deleteSelector of selectors.DELETE_BUTTON) { deleteButton = dropdown.querySelector(deleteSelector); if (deleteButton) { break; } } if (deleteButton) break; } } }, 1000); } } if (deleteButton) { (deleteButton as HTMLElement).click(); return { noteId: currentNoteId, title, found: true }; } else { return { noteId: currentNoteId, title, found: false, error: 'Delete button not found' }; } } } return { noteId: targetNoteId, title: '', found: false, error: 'Note not found' }; }, DELETE_SELECTORS, noteId ); if (!result.found) { throw new DeleteError(result.error || 'Note not found', { noteId }); } // Wait for confirmation dialog if it appears await sleep(1000); // Handle confirmation dialog await this.handleConfirmationDialog(page); return { noteId: result.noteId, title: result.title }; } catch (error) { throw new DeleteError( 'Failed to find and delete note', { noteId }, error instanceof Error ? error : new Error(String(error)) ); } } /** * Find and delete the last published note */ private async findAndDeleteLastNote(page: Page): Promise<{ noteId: string; title: string }> { try { const result = await page.evaluate((selectors: typeof DELETE_SELECTORS) => { // Try each note item selector let noteElements: Element[] = []; for (const selector of selectors.NOTE_ITEM) { const elements = Array.from(document.querySelectorAll(selector)); if (elements.length > 0) { noteElements = elements; break; } } if (noteElements.length === 0) { return { noteId: '', title: '', found: false, error: 'No notes found' }; } // Get the first note (most recent) const firstNote = noteElements[0]; // Extract note ID let noteId = ''; const impressionData = firstNote.getAttribute('data-impression'); if (impressionData) { try { const parsed = JSON.parse(impressionData); noteId = parsed?.noteTarget?.value?.noteId || ''; } catch (e) { // Ignore parsing errors } } // Also try to find note ID in link if (!noteId) { const linkElement = firstNote.querySelector('a[href*="/explore/"], a[href*="/note/"]'); if (linkElement) { const href = linkElement.getAttribute('href') || ''; const match = href.match(/\/explore\/([a-zA-Z0-9]+)/); if (match) { noteId = match[1]; } } } // Extract title const titleElement = firstNote.querySelector('[class*="title"], [class*="name"]'); const title = titleElement?.textContent?.trim() || 'Unknown'; // Look for delete button or more options button let deleteButton: Element | null = null; // Try each delete button selector for (const selector of selectors.DELETE_BUTTON) { deleteButton = firstNote.querySelector(selector); if (deleteButton) { break; } } if (!deleteButton) { // Try to find more options button first let moreButton: Element | null = null; for (const selector of selectors.MORE_OPTIONS) { moreButton = firstNote.querySelector(selector); if (moreButton) { break; } } if (moreButton) { (moreButton as HTMLElement).click(); // Wait for dropdown to appear and look for delete option setTimeout(() => { for (const selector of selectors.DROPDOWN_MENU) { const dropdown = document.querySelector(selector); if (dropdown) { for (const deleteSelector of selectors.DELETE_BUTTON) { deleteButton = dropdown.querySelector(deleteSelector); if (deleteButton) { break; } } if (deleteButton) break; } } }, 1000); } } if (deleteButton) { (deleteButton as HTMLElement).click(); return { noteId, title, found: true }; } else { return { noteId, title, found: false, error: 'Delete button not found' }; } }, DELETE_SELECTORS); if (!result.found) { throw new DeleteError(result.error || 'Failed to find delete button', {}); } // Wait for confirmation dialog if it appears await sleep(1000); // Handle confirmation dialog await this.handleConfirmationDialog(page); return { noteId: result.noteId, title: result.title }; } catch (error) { throw new DeleteError( 'Failed to find and delete last note', {}, error instanceof Error ? error : new Error(String(error)) ); } } /** * Handle confirmation dialog for delete operation */ private async handleConfirmationDialog(page: Page): Promise<void> { try { // Wait for confirmation dialog to appear await sleep(2000); // Look for confirmation buttons using all possible selectors let confirmButton = null; // Try each confirm button selector for (const selector of DELETE_SELECTORS.CONFIRM_BUTTON) { confirmButton = await page.$(selector); if (confirmButton) { logger.info(`Found confirm button with selector: ${selector}`); break; } } // If not found, try modal confirm selectors if (!confirmButton) { for (const selector of DELETE_SELECTORS.MODAL_CONFIRM) { confirmButton = await page.$(selector); if (confirmButton) { logger.info(`Found modal confirm button with selector: ${selector}`); break; } } } if (confirmButton) { logger.info('Clicking confirmation button'); await confirmButton.click(); await sleep(2000); } else { // If no confirmation dialog, the delete might have been immediate logger.info('No confirmation dialog found, delete might be immediate'); } } catch (error) { logger.warn(`Failed to handle confirmation dialog: ${error}`); // Continue anyway as the delete might still work } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Algovate/xhs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server