From 23fc89d33d9abfd38b5ce91351d8ca00fe849f6b Mon Sep 17 00:00:00 2001 From: Haochen Duan Date: Sun, 21 Jan 2024 11:17:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9notes=E7=9A=84?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/main/src/channelEnum.ts | 3 +- .../main/src/ipcHandle/xiaohongshuHandle.mts | 23 +++++++++- .../expand/xiaohongshu/XiaohongshuExpand.ts | 33 ++++++++++++- .../expand/xiaohongshu/function/parseHtml.ts | 46 +++++++++++++++++++ .../xiaohongshu.worker/messageTypes.ts | 12 ++++- .../xiaohongshu.worker/xiaohongshu.worker.ts | 27 +++++++++-- .../qqtools/src/services/xiaohongshu/index.ts | 6 ++- 7 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 packages/qqtools/src/QQ/function/expand/xiaohongshu/function/parseHtml.ts diff --git a/packages/main/src/channelEnum.ts b/packages/main/src/channelEnum.ts index 471d6b56..3006da1f 100644 --- a/packages/main/src/channelEnum.ts +++ b/packages/main/src/channelEnum.ts @@ -13,7 +13,8 @@ export const enum XiaohongshuHandleChannel { XiaohongshuDestroy = 'xiaohongshu-destroy', XiaohongshuChromeRemoteInit = 'xiaohongshu-chrome-remote-init', XiaohongshuChromeRemoteCookie = 'xiaohongshu-chrome-remote-cookie', - XiaohongshuChromeRemoteSign = 'xiaohongshu-chrome-remote-sign' + XiaohongshuChromeRemoteSign = 'xiaohongshu-chrome-remote-sign', + XiaohongshuChromeRemoteRequestHtml = 'xiaohongshu-chrome-remote-request-html' } // Electron ipc channel diff --git a/packages/main/src/ipcHandle/xiaohongshuHandle.mts b/packages/main/src/ipcHandle/xiaohongshuHandle.mts index fcd0f7a3..01e52edf 100644 --- a/packages/main/src/ipcHandle/xiaohongshuHandle.mts +++ b/packages/main/src/ipcHandle/xiaohongshuHandle.mts @@ -1,5 +1,4 @@ import * as path from 'node:path'; -import * as process from 'node:process'; import { setTimeout } from 'node:timers'; import * as fsP from 'node:fs/promises'; import { ipcMain, BrowserWindow, type IpcMainInvokeEvent, type Cookie } from 'electron'; @@ -135,6 +134,28 @@ function ipcXiaohongshuHandle(): void { return undefined; }); + + ipcMain.handle( + XiaohongshuHandleChannel.XiaohongshuChromeRemoteRequestHtml, + async function(event: IpcMainInvokeEvent, url: string): Promise { + await clientSwitch(client, 'www.xiaohongshu.com'); + + if (client) { + const signResult: Protocol.Runtime.EvaluateResponse = await client.Runtime.evaluate({ + expression: `async function requestHtml() { + var request = await fetch('${ url }', { credentials: 'include' }); + return request.text(); + } + requestHtml();`, + awaitPromise: true + }); + + return signResult.result.value; + } + + return undefined; + } + ); } export default ipcXiaohongshuHandle; \ No newline at end of file diff --git a/packages/qqtools/src/QQ/function/expand/xiaohongshu/XiaohongshuExpand.ts b/packages/qqtools/src/QQ/function/expand/xiaohongshu/XiaohongshuExpand.ts index 91d08b74..67c5a08e 100644 --- a/packages/qqtools/src/QQ/function/expand/xiaohongshu/XiaohongshuExpand.ts +++ b/packages/qqtools/src/QQ/function/expand/xiaohongshu/XiaohongshuExpand.ts @@ -2,8 +2,9 @@ import { ipcRenderer } from 'electron'; import { message } from 'antd'; import type { MessageInstance } from 'antd/es/message/interface'; import { XiaohongshuHandleChannel } from '@qqtools3/main/src/channelEnum'; -import type { SignResult } from '@qqtools-api/xiaohongshu'; +import type { SignResult, UserPostedResponse, PostedNoteItem } from '@qqtools-api/xiaohongshu'; import getXiaohongshuWorker from './xiaohongshu.worker/getXiaohongshuWorker'; +import { parseHtml, type XiaohongshuInitialState, type NoteItem } from './function/parseHtml'; import type { QQProtocol, QQModals } from '../../../QQBotModals/ModalTypes'; import type { OptionsItemXiaohongshu } from '../../../../commonTypes'; import type { XHSProtocol } from './xiaohongshu.worker/messageTypes'; @@ -47,6 +48,30 @@ class XiaohongshuExpand { return JSON.parse(result); } + static async chromeDevtoolsRequestHtml(u: string): Promise { + const result: string | void = await ipcRenderer.invoke(XiaohongshuHandleChannel.XiaohongshuChromeRemoteRequestHtml, u); + + if (result) { + const initialState: XiaohongshuInitialState | undefined = parseHtml(result); + + if (initialState) { + return { + code: 0, + success: true, + data: { + cursor: '', + has_more: false, + notes: initialState.user.notes.flat().map((o: NoteItem): PostedNoteItem => ({ + type: o.noteCard.type, + note_id: o.noteCard.noteId, + cover: { url: o.noteCard.cover.urlDefault } + })) + } + }; + } + } + } + // 图片转base64 static imageToBase64(url: string): Promise { return new Promise((resolve: Function, reject: Function): void => { @@ -108,6 +133,12 @@ class XiaohongshuExpand { id: event.data.id, result: await XiaohongshuExpand.imageToBase64(event.data.imageUrl) }); + } else if (event.data.type === 'requestHtml') { + this.xiaohongshuWorker?.postMessage({ + type: 'requestHtml', + id: event.data.id, + result: await XiaohongshuExpand.chromeDevtoolsRequestHtml(event.data.url) + }); } }; diff --git a/packages/qqtools/src/QQ/function/expand/xiaohongshu/function/parseHtml.ts b/packages/qqtools/src/QQ/function/expand/xiaohongshu/function/parseHtml.ts new file mode 100644 index 00000000..dbd8c34a --- /dev/null +++ b/packages/qqtools/src/QQ/function/expand/xiaohongshu/function/parseHtml.ts @@ -0,0 +1,46 @@ +export interface NoteItem { + id: string; + noteCard: { + type: 'normal' | 'video'; + noteId: string; + cover: { + urlDefault: string; + urlPre: string; + }; + displayTitle: string; + user: { + avatar: string; + nickName: string; + nickname: string; + userId: string; + }; + }; +} + +export interface XiaohongshuInitialState { + user: { + notes: Array>; + }; +} + +/* 解析小红书的html并获取window.__INITIAL_STATE__的值 */ +export function parseHtml(html: string): XiaohongshuInitialState | undefined { + const parseDocument: Document = new DOMParser().parseFromString(html, 'text/html'); + const scripts: NodeListOf = parseDocument.querySelectorAll('script'); + let initialState: XiaohongshuInitialState | undefined = undefined; + + for (const script of scripts) { + const scriptStr: string = script.innerHTML; + + if (/^window\._{2}INITIAL_STATE_{2}\s*=\s*.+$/.test(scriptStr)) { + const str: string = scriptStr + .replace(/window\._{2}INITIAL_STATE_{2}\s*=\s*/, ''); // 剔除"="前面的字符串 + + // eslint-disable-next-line no-eval + initialState = eval(`(${ str })`); + break; + } + } + + return initialState; +} \ No newline at end of file diff --git a/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/messageTypes.ts b/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/messageTypes.ts index 15b9489f..1812f165 100644 --- a/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/messageTypes.ts +++ b/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/messageTypes.ts @@ -38,7 +38,14 @@ export interface ImageToBase64Message { result: string; } -export type MessageObject = CloseMessage | BaseInitMessage | SignMessage | ImageToBase64Message; +// 获取html +export interface RequestHtmlMessage { + type: 'requestHtml'; + id: string; + result: string; +} + +export type MessageObject = CloseMessage | BaseInitMessage | SignMessage | ImageToBase64Message | RequestHtmlMessage; // 判断各种类型 type IsMessageFunction = (d: MessageObject) => d is T; @@ -50,6 +57,9 @@ export const isSignMessage: IsMessageFunction = (d: MessageObject): export const isImageToBase64Message: IsMessageFunction = (d: MessageObject): d is ImageToBase64Message => d['type'] === 'imageToBase64'; +export const isRequestHtmlMessage: IsMessageFunction + = (d: MessageObject): d is RequestHtmlMessage => d['type'] === 'requestHtml'; + // 组合数据 export interface MergeData { noteId: string; diff --git a/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/xiaohongshu.worker.ts b/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/xiaohongshu.worker.ts index d4129f09..bc3a6660 100644 --- a/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/xiaohongshu.worker.ts +++ b/packages/qqtools/src/QQ/function/expand/xiaohongshu/xiaohongshu.worker/xiaohongshu.worker.ts @@ -17,6 +17,7 @@ import { isCloseMessage, isSignMessage, isImageToBase64Message, + isRequestHtmlMessage, XHSProtocol, type MessageObject, type BaseInitMessage, @@ -72,6 +73,22 @@ function imageToBase64(imageUrl: string): Promise { }); } +function requestHtml(): Promise { + const id: string = `${ Math.random() }`; + + return new Promise((resolve: Function, reject: Function): void => { + function handleSignMessage(event: MessageEvent): void { + if (isRequestHtmlMessage(event.data) && id === event.data.id) { + removeEventListener('message', handleSignMessage); + resolve(event.data.result); + } + } + + addEventListener('message', handleSignMessage); + postMessage({ id, url: `https://www.xiaohongshu.com/user/profile/${ userId }`, type: 'requestHtml' }); + }); +} + /* 发送数据 */ async function QQSendGroup(item: Required): Promise { const sendMessageGroup: Array = [ @@ -182,9 +199,9 @@ async function getMergeData(data: Array): Promise { try { - const userPostedRes: UserPostedResponse = await requestUserPosted(userId, cookieString, signProtocol, port); + const userPostedRes: UserPostedResponse | undefined = await requestHtml(); - if (userPostedRes.success) { + if (userPostedRes && userPostedRes.success) { _isSendDebugMessage && (_debugTimes = 0); const data: Array = userPostedRes.data.notes ?? []; @@ -252,9 +269,9 @@ EndTime: ${ _endTime }`, /* 初始化小红书 */ async function xiaohongshuInit(): Promise { try { - const userPostedRes: UserPostedResponse = await requestUserPosted(userId, cookieString, signProtocol, port); + const userPostedRes: UserPostedResponse | undefined = await requestHtml(); - if (userPostedRes.success) { + if (userPostedRes && userPostedRes.success) { const data: Array = userPostedRes.data.notes ?? []; if (data.length) { @@ -283,7 +300,7 @@ addEventListener('message', function(event: MessageEvent): void { try { xiaohongshuTimer && clearTimeout(xiaohongshuTimer); } catch { /* noop */ } - } else if (isSignMessage(event.data) || isImageToBase64Message(event.data)) { + } else if (isSignMessage(event.data) || isImageToBase64Message(event.data) || isRequestHtmlMessage(event.data)) { /* noop */ } else { const { diff --git a/packages/qqtools/src/services/xiaohongshu/index.ts b/packages/qqtools/src/services/xiaohongshu/index.ts index 008ca7f1..b2624156 100644 --- a/packages/qqtools/src/services/xiaohongshu/index.ts +++ b/packages/qqtools/src/services/xiaohongshu/index.ts @@ -67,7 +67,11 @@ export async function requestFeed( userId: string ): Promise { const reqPath: string = '/api/sns/web/v1/feed'; - const json: { source_note_id: string } = { source_note_id: sourceNoteId }; + const json: Record = { + source_note_id: sourceNoteId, + image_formats: ['jpg', 'webp', 'avif'], + extra: { need_body_topic: 1 } + }; const headers: SignResult = signProtocol === XHSProtocol.ChromeDevtoolsProtocol ? await invokeSign(reqPath, JSON.stringify(json)) : await requestSign(port, reqPath, json);