返回 AiToEarn
1 import { screen, BrowserWindow, net, session } from 'electron';
2 import * as crypto from 'crypto';
3 import sizeOf from 'image-size';
4 import { CommonUtils } from '../../util/common';
5 import { FileUtils } from '../../util/file';
6 import { CookieToString, getFileContent } from '../utils';
7 import requestNet from '../requestNet';
8 import {
9 IXHSGetWorksResponse,
10 IXHSLocationResponse,
11 IXHSTopicsResponse,
12 XhsCommentListResponse,
13 XhsCommentPostResponse,
14 XiaohongshuApiResponse,
15 } from './xiaohongshu.type';
16 import { RetryWhile } from '../../../commont/utils';
17 import { logger } from '../../global/log';
18
19 export type XSLPlatformSettingType = {
20 // 标题
21 title?: string;
22 // 描述
23 desc?: string;
24 // 定时发布
25 timingTime?: number;
26 // @用户
27 mentionedUserInfo?: {
28 nickName: string;
29 uid: string;
30 }[];
31 // 话题
32 topicsDetail?: {
33 topicId: string;
34 topicName: string;
35 }[];
36 // 位置
37 poiInfo?: {
38 poiType: number;
39 poiId: string;
40 poiName: string;
41 poiAddress: string;
42 };
43 cover: string;
44 // 0 公共 1 私密 4 好友
45 visibility_type: 0 | 1 | 4;
46 proxy: string;
47 };
48
49 const esec_token = 'ABrmhLmsdmsu9bCQ80qvGPN2CYSjEqwi5G1l2dirNUjaw%3D';
50
51 export class XiaohongshuService {
52 private defaultUserAgent =
53 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
54 private loginUrl = 'https://creator.xiaohongshu.com/';
55 private loginUrlHome = 'https://www.xiaohongshu.com/';
56 private getUserInfoUrl =
57 'https://edith.xiaohongshu.com/api/sns/web/v2/user/me';
58 private getDashboardUrl =
59 'https://creator.xiaohongshu.com/api/galaxy/v2/creator/datacenter/account/base';
60 private getFansInfoUrl =
61 'https://creator.xiaohongshu.com/api/galaxy/creator/home/personal_info';
62 private getUploadPermitUrl =
63 'https://creator.xiaohongshu.com/api/media/v1/upload/web/permit';
64 private postCreateVideoUrl =
65 'https://edith.xiaohongshu.com/web_api/sns/v2/note';
66 private fileBlockSize = 5242880;
67 private cookieCheckField = 'access-token';
68 private cookieIntervalList: { [key: string]: NodeJS.Timer } = {};
69 private prev_web_session = '';
70 private win?: BrowserWindow;
71 private callback?: (progress: number, msg?: string) => void;
72
73 /**
74 * 授权|预览
75 */
76 async loginOrView(
77 authModel: 'login' | 'view',
78 cookies?: any,
79 ): Promise<{
80 success: boolean;
81 data?: { cookie: any; userInfo: any };
82 error?: string;
83 }> {
84 try {
85 const winRes = await this.createAuthorizationWindow(
86 authModel === 'view' ? cookies : null,
87 );
88 const { winContentsId, partition } = winRes;
89 const newCookies = await this.filterCookie(winContentsId, partition);
90 const userInfo = await this.getUserInfo(newCookies);
91 if (authModel === 'login') {
92 const winBrowserWindow = BrowserWindow.fromId(winContentsId);
93 winBrowserWindow?.close();
94 this.prev_web_session = '';
95 }
96
97 const result = {
98 success: true,
99 data: {
100 cookie: newCookies,
101 userInfo: userInfo,
102 },
103 };
104 return result;
105 } catch (error) {
106 return {
107 success: false,
108 error: error instanceof Error ? error.message : 'Login failed',
109 };
110 }
111 }
112
113 /**
114 * 创建授权窗口
115 */
116 private async createAuthorizationWindow(cookies: any = null) {
117 // 生成随机partition
118 const partition = Date.now().toString();
119
120 // 获取屏幕尺寸
121 const { width, height } = screen.getPrimaryDisplay().workAreaSize;
122
123 // 创建窗口
124 const win = new BrowserWindow({
125 width: Math.ceil(width * 0.8),
126 height: Math.ceil(height * 0.8),
127 show: false,
128 webPreferences: {
129 contextIsolation: false,
130 nodeIntegration: false,
131 partition: partition,
132 },
133 });
134 win.show();
135
136 // 设置用户代理
137 win.webContents.setUserAgent(this.defaultUserAgent);
138 this.prev_web_session = '';
139
140 // 如果有cookies,设置cookies
141
142 // 加载登录页
143 await win.loadURL(this.loginUrlHome);
144
145 this.win = win;
146 return {
147 winContentsId: win.id,
148 partition,
149 };
150 }
151
152 /**
153 * Filter and monitor cookies until login is detected
154 */
155 private async filterCookie(
156 winContentsId: number,
157 partition: string,
158 ): Promise<Electron.Cookie[]> {
159 return new Promise((resolve, reject) => {
160 // Monitor cookie status with interval
161 this.cookieIntervalList[winContentsId] = setInterval(async () => {
162 try {
163 if (this.win!.webContents.getURL().includes(this.loginUrlHome)) {
164 const cookies2 = await session
165 .fromPartition(partition)
166 .cookies.get({
167 url: this.loginUrlHome,
168 });
169 const web_session = cookies2.find((v) => v.name === 'web_session');
170 if (!this.prev_web_session) {
171 this.prev_web_session = web_session?.value || '';
172 }
173 if (this.prev_web_session === (web_session?.value || '')) return;
174 await this.win!.loadURL(this.loginUrl + 'login?source=official');
175 } else if (this.win!.webContents.getURL().includes(this.loginUrl)) {
176 const cookies1 = await session
177 .fromPartition(partition)
178 .cookies.get({
179 url: this.loginUrl,
180 });
181 const cookies2 = await session
182 .fromPartition(partition)
183 .cookies.get({
184 url: this.loginUrlHome,
185 });
186 const cookies = cookies1.concat(cookies2);
187 const alreadyLogin = cookies1.some((cookie) => {
188 return cookie.name.includes(this.cookieCheckField);
189 });
190 if (alreadyLogin) {
191 // Clear interval
192 if (this.cookieIntervalList[winContentsId]) {
193 clearInterval(this.cookieIntervalList[winContentsId] as any);
194 delete this.cookieIntervalList[winContentsId];
195 }
196
197 resolve(cookies);
198 }
199 }
200 } catch (error) {
201 // Clear interval on error
202 if (this.cookieIntervalList[winContentsId]) {
203 clearInterval(this.cookieIntervalList[winContentsId] as any);
204 delete this.cookieIntervalList[winContentsId];
205 }
206 console.error('Failed to get cookies:', error);
207 reject(new Error('Failed to get website cookies'));
208 }
209 }, 1500); // Check every 3 seconds
210 });
211 }
212
213 /**
214 * Cleanup method to clear any remaining intervals
215 */
216 private clearCookieIntervals() {
217 Object.entries(this.cookieIntervalList).forEach(([winId, interval]) => {
218 clearInterval(interval as any);
219 delete this.cookieIntervalList[winId];
220 });
221 }
222
223 /**
224 * Override class destructor to ensure cleanup
225 */
226 public destroy() {
227 this.clearCookieIntervals();
228 }
229
230 /**
231 * 获取用户信息
232 */
233 public async getUserInfo(cookies: Electron.Cookie[]) {
234 const cookieString = cookies
235 .map((cookie) => `${cookie.name}=${cookie.value}`)
236 .join('; ');
237
238 const userInfo = await this.makeRequest(
239 this.getUserInfoUrl,
240 {
241 method: 'GET',
242 headers: {
243 Cookie: cookieString,
244 Referer: this.loginUrl,
245 },
246 },
247 '',
248 );
249
250 const fansInfo = await this.makeRequest(
251 this.getFansInfoUrl,
252 {
253 method: 'GET',
254 headers: {
255 Cookie: cookieString,
256 Referer: this.loginUrl,
257 },
258 },
259 '',
260 );
261
262 return {
263 authorId: userInfo.data.user_id || '',
264 nickname: userInfo.data.nickname || '',
265 avatar: userInfo.data.imageb || '',
266 fansCount: fansInfo.data.fans_count || 0,
267 diagnosis_status: fansInfo.data.diagnosis_status,
268 };
269 }
270
271 /**
272 * 获取账户数据
273 */
274 public async getDashboardFunc(
275 cookies: Electron.Cookie[],
276 startDate?: string,
277 endDate?: string,
278 ) {
279 // 初始化cookie
280 const cookieString = CommonUtils.convertCookieToJson(cookies);
281
282 // 获取cookie_a1
283 const cookieObject = cookies;
284 let cookie_a1 = null;
285 for (const cookieItem of cookieObject) {
286 if (cookieItem.name === 'a1') {
287 cookie_a1 = cookieItem.value;
288 break;
289 }
290 }
291
292 const reverseRes: any = await this.getReverseResult({
293 url: '/api/galaxy/v2/creator/datacenter/account/base',
294 a1: cookie_a1,
295 });
296
297 const userInfo = await this.makeRequest(
298 this.getDashboardUrl,
299 {
300 method: 'GET',
301 headers: {
302 'Content-Type': 'application/json;charset=UTF-8',
303 Cookie: cookieString,
304 Referer: 'https://creator.xiaohongshu.com/statistics/account',
305 userAgent:
306 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
307 'X-S': reverseRes['X-s'],
308 'X-T': reverseRes['X-t'],
309 },
310 },
311 '',
312 );
313
314 if (userInfo.code == 0) {
315 if (startDate && endDate) {
316 // 处理30天的数据
317 const dataList = [];
318 const startTimestamp = new Date(startDate).getTime();
319 const endTimestamp = new Date(endDate).getTime() + 1;
320
321 // 获取所有列表数据
322 const rise_fans_list = userInfo.data.thirty.rise_fans_list || [];
323 const view_list = userInfo.data.thirty.view_list || [];
324 const comment_list = userInfo.data.thirty.comment_list || [];
325 const like_list = userInfo.data.thirty.like_list || [];
326 const home_view_list = userInfo.data.thirty.home_view_list || [];
327
328 // 创建日期映射
329 const dateMap: { [key: string]: any } = {};
330
331 // 处理所有类型的数据
332 [
333 { list: rise_fans_list, key: 'zhangfen' },
334 { list: view_list, key: 'bofang' },
335 { list: comment_list, key: 'pinglun' },
336 { list: like_list, key: 'dianzan' },
337 { list: rise_fans_list, key: 'fenxiang' },
338 { list: home_view_list, key: 'zhuye' },
339 ].forEach(({ list, key }) => {
340 list.forEach((item: any) => {
341 const timestamp = item.date;
342 // 检查日期是否在范围内
343 if (timestamp >= startTimestamp && timestamp <= endTimestamp) {
344 if (!dateMap[timestamp]) {
345 dateMap[timestamp] = {
346 date: new Date(timestamp).toISOString().split('T')[0],
347 zhangfen: 0,
348 bofang: 0,
349 pinglun: 0,
350 dianzan: 0,
351 fenxiang: 0,
352 zhuye: 0,
353 };
354 }
355 dateMap[timestamp][key] = item.count;
356 }
357 });
358 });
359
360 // 转换为数组并排序
361 const sortedData = Object.values(dateMap).sort(
362 (a: any, b: any) =>
363 new Date(b.date).getTime() - new Date(a.date).getTime(),
364 );
365
366 return {
367 success: true,
368 data: sortedData,
369 };
370 } else {
371 // 保持原有的单日数据逻辑
372 const data = {
373 zhangfen: userInfo.data.seven.rise_fans_list[0].count,
374 bofang: userInfo.data.seven.view_list[0].count,
375 pinglun: userInfo.data.seven.comment_list[0].count,
376 dianzan: userInfo.data.seven.like_list[0].count,
377 fenxiang: userInfo.data.seven.rise_fans_list[0].count,
378 zhuye: userInfo.data.seven.home_view_list[0].count,
379 };
380
381 return {
382 success: true,
383 data: [data],
384 };
385 }
386 } else {
387 return {
388 success: false,
389 data: userInfo,
390 };
391 }
392 }
393
394 /**
395 * 通用请求方法
396 */
397 private async makeRequest(
398 url: string,
399 options: any,
400 proxy: string,
401 ): Promise<any> {
402 return new Promise(async (resolve, reject) => {
403 try {
404 const res = await requestNet({
405 url: url,
406 method: options.method,
407 headers: options.headers,
408 body: options.data,
409 proxy,
410 });
411 resolve(res.data);
412 } catch (e) {
413 reject(e);
414 }
415 });
416 }
417
418 /**
419 * 获取上传许可证
420 * @param cookieString
421 * @param scene
422 */
423 async getUploadPermit(cookieString: string, scene: string, proxy: string) {
424 return new Promise(async (resolve, reject) => {
425 try {
426 const permitRes = await this.makeRequest(
427 this.getUploadPermitUrl +
428 `?biz_name=spectrum&scene=${scene}&file_count=1&version=1&source=web`,
429 {
430 method: 'GET',
431 headers: {
432 Cookie: cookieString,
433 Referer: this.loginUrl,
434 },
435 },
436 proxy,
437 );
438
439 if (permitRes.code !== 0) {
440 reject('获取上传许可证失败,失败原因:' + permitRes.msg);
441 }
442
443 const uploadTempPermits = permitRes.data.uploadTempPermits;
444 resolve(uploadTempPermits);
445 } catch (err: any) {
446 let errorMessage;
447 if (err && err.message) {
448 errorMessage = err.message;
449 } else if (err) {
450 errorMessage = err;
451 } else {
452 errorMessage = '未知';
453 }
454 reject('获取上传许可证失败,失败原因:' + errorMessage);
455 }
456 });
457 }
458
459 /**
460 * 上传文件到远程服务器
461 * @param url 上传地址
462 * @param fileContent 文件内容
463 * @param headers 请求头
464 * @param proxy
465 */
466 private async uploadFile(
467 url: string,
468 fileContent: Buffer,
469 headers: any,
470 proxy: string,
471 ): Promise<any> {
472 return new Promise(async (resolve, reject) => {
473 try {
474 const res = await requestNet({
475 url: url,
476 method: 'PUT',
477 headers: headers,
478 isFile: true,
479 body: fileContent,
480 proxy,
481 });
482 resolve(res);
483 } catch (e) {
484 console.error('上传文件失败:', e);
485 reject(e);
486 }
487 });
488 }
489
490 /**
491 * 上传封面文件
492 * @param cookieString
493 * @param filePath
494 */
495 async uploadCoverFile(
496 cookieString: string,
497 filePath: string,
498 proxy: string,
499 ): Promise<{
500 coverUploadFileId: string;
501 coverDimensions: any;
502 remotePreviewUrl: string;
503 }> {
504 return new Promise(async (resolve, reject) => {
505 try {
506 // 获取文件上传许可证
507 const uploadPermit: any = await this.getUploadPermit(
508 cookieString,
509 'image',
510 proxy,
511 );
512 const coverUploadFileId = uploadPermit[0].fileIds[0];
513 const uploadAddr = uploadPermit[0].uploadAddr;
514 const uploadToken = uploadPermit[0].token;
515 const uploadBaseUrl = `https://${uploadAddr}/${coverUploadFileId}`;
516
517 // 获取文件内容
518 const fileContent = await getFileContent(filePath);
519
520 // 获取宽高信息
521 const coverDimensions = sizeOf(fileContent);
522
523 // 直接上传
524 let uploadRes = await this.uploadFile(
525 uploadBaseUrl,
526 fileContent,
527 {
528 Referer: this.loginUrl,
529 'X-Cos-Security-Token': uploadToken,
530 },
531 proxy,
532 );
533
534 uploadRes = uploadRes.headers;
535
536 if (!uploadRes.hasOwnProperty('x-ros-preview-url')) {
537 reject('上传封面失败,失败原因:未获取到previewUrl');
538 return;
539 }
540 const remotePreviewUrl = uploadRes['x-ros-preview-url'];
541
542 // 上传成功,返回结果
543 resolve({
544 coverUploadFileId,
545 remotePreviewUrl,
546 coverDimensions,
547 });
548 } catch (err: any) {
549 let errorMessage;
550 if (err && err.message) {
551 errorMessage = err.message;
552 } else if (err) {
553 errorMessage = err;
554 } else {
555 errorMessage = '未知';
556 }
557 reject('上传封面失败,失败原因:' + errorMessage);
558 }
559 });
560 }
561
562 /**
563 * 文本响应专用请求方法
564 */
565 private async makeTextRequest(url: string, options: any): Promise<any> {
566 return new Promise((resolve, reject) => {
567 const request = net.request({
568 method: options.method,
569 url: url,
570 headers: options.headers,
571 });
572
573 // 发送请求体
574 if (options.data) {
575 request.write(
576 typeof options.data === 'string'
577 ? options.data
578 : JSON.stringify(options.data),
579 );
580 }
581
582 request.on('response', (response) => {
583 let data = '';
584 response.on('data', (chunk) => {
585 data += chunk;
586 });
587 response.on('end', () => {
588 resolve({
589 data,
590 headers: response.headers,
591 status: response.statusCode,
592 });
593 });
594 });
595
596 request.on('error', (error) => {
597 console.error('Request error:', error);
598 reject(error);
599 });
600 request.end();
601 });
602 }
603
604 /**
605 * 上传视频文件
606 * @param cookieString
607 * @param filePath
608 * @param filePartInfo
609 * @param fileInfo
610 */
611 private async uploadVideoFile(
612 cookieString: string,
613 filePath: string,
614 filePartInfo: any,
615 fileInfo: any,
616 proxy: string,
617 ): Promise<{
618 uploadFileId: string;
619 remotePreviewUrl: string;
620 remoteVideoId: string;
621 }> {
622 return new Promise(async (resolve, reject) => {
623 try {
624 // 获取文件上传许可证
625 const uploadPermit: any = await this.getUploadPermit(
626 cookieString,
627 'video',
628 proxy,
629 );
630 const uploadFileId = uploadPermit[0].fileIds[0];
631 const uploadAddr = uploadPermit[0].uploadAddr;
632 const uploadToken = uploadPermit[0].token;
633 const uploadBaseUrl = `https://${uploadAddr}/${uploadFileId}`;
634 let remotePreviewUrl = '';
635 let remoteVideoId = '';
636
637 // 开始上传文件
638 if (filePartInfo.blockInfo?.length === 1) {
639 // 获取文件内容
640 const fileContent = await FileUtils.getFilePartContent(
641 filePath,
642 0,
643 filePartInfo.fileSize - 1,
644 );
645 // 直接上传
646 let uploadRes = await this.uploadFile(
647 uploadBaseUrl,
648 fileContent,
649 {
650 'Content-Type': fileInfo.mimeType,
651 Referer: this.loginUrl,
652 'X-Cos-Security-Token': uploadToken,
653 },
654 proxy,
655 );
656
657 uploadRes = uploadRes.headers;
658 if (
659 !uploadRes.hasOwnProperty('x-ros-preview-url') ||
660 !uploadRes.hasOwnProperty('x-ros-video-id')
661 ) {
662 reject('上传视频失败,失败原因:未获取到videoId');
663 return;
664 }
665 remotePreviewUrl = uploadRes['x-ros-preview-url'];
666 remoteVideoId = uploadRes['x-ros-video-id'];
667 } else {
668 // 获取分片上传ID
669 const uploadIdRes = await this.makeTextRequest(
670 uploadBaseUrl + '?uploads',
671 {
672 method: 'POST',
673 headers: {
674 'Content-Type': fileInfo.mimeType,
675 Referer: this.loginUrl,
676 'X-Cos-Security-Token': uploadToken,
677 },
678 },
679 );
680
681 if (CommonUtils.isJsonString(uploadIdRes.data)) {
682 const parsedRes = JSON.parse(uploadIdRes.data);
683 reject('上传视频失败,失败原因:获取上传id失败:' + parsedRes.msg);
684 return;
685 }
686
687 const parsedXml = (await CommonUtils.xml2json(uploadIdRes.data)) as {
688 InitiateMultipartUploadResult: {
689 UploadId: string[];
690 };
691 };
692 const uploadId =
693 parsedXml.InitiateMultipartUploadResult.UploadId[0] ?? '';
694 if (uploadId === '') {
695 reject('上传视频失败,失败原因:获取上传id失败');
696 return;
697 }
698
699 // 分片上传文件
700 const uploadPartInfo: any[] = [];
701
702 for (const i in filePartInfo.blockInfo) {
703 if (this.callback)
704 this.callback(
705 50,
706 `上传视频(${i}/${filePartInfo.blockInfo.length})`,
707 );
708 const isSuccess = await RetryWhile(async () => {
709 const chunkStart =
710 i === '0' ? 0 : filePartInfo.blockInfo[parseInt(i) - 1];
711 const chunkEnd = filePartInfo.blockInfo[i] - 1;
712 const chunkContent = await FileUtils.getFilePartContent(
713 filePath,
714 chunkStart,
715 chunkEnd,
716 );
717
718 // 开始上传
719 const uploadPartRes = await this.uploadFile(
720 uploadBaseUrl +
721 `?uploadId=${uploadId}&partNumber=${parseInt(i) + 1}`,
722 chunkContent,
723 {
724 Referer: this.loginUrl,
725 'X-Cos-Security-Token': uploadToken,
726 },
727 proxy,
728 );
729
730 const headers = uploadPartRes.headers;
731 if (!headers.hasOwnProperty('etag') || headers['etag'] === '') {
732 return false;
733 }
734
735 // 分片上传成功
736 uploadPartInfo.push({
737 Part: {
738 PartNumber: parseInt(i) + 1,
739 ETag: headers['etag'],
740 },
741 });
742 return true;
743 }, 3);
744
745 if (!isSuccess) {
746 reject('上传视频失败,失败原因:上传分片失败');
747 break;
748 }
749 }
750
751 // 合并分片
752 const completeXml = await CommonUtils.json2xml(uploadPartInfo);
753 const completeRes = await this.makeTextRequest(
754 uploadBaseUrl + `?uploadId=${uploadId}`,
755 {
756 method: 'POST',
757 headers: {
758 Referer: this.loginUrl,
759 'X-Cos-Security-Token': uploadToken,
760 'Content-Type': 'application/xml',
761 },
762 data: completeXml,
763 },
764 );
765
766 if (CommonUtils.isJsonString(completeRes.data)) {
767 const parsedRes = JSON.parse(completeRes.data);
768 reject('上传视频失败,失败原因:合并分片失败:' + parsedRes.msg);
769 return;
770 }
771
772 const headers = completeRes.headers;
773 if (
774 !headers.hasOwnProperty('x-ros-preview-url') ||
775 !headers.hasOwnProperty('x-ros-video-id')
776 ) {
777 reject('上传视频失败,失败原因:未获取到videoId');
778 return;
779 }
780
781 remotePreviewUrl = headers['x-ros-preview-url'];
782 remoteVideoId = headers['x-ros-video-id'];
783 }
784
785 // 上传成功,返回结果
786 resolve({
787 uploadFileId: uploadFileId,
788 remotePreviewUrl: remotePreviewUrl,
789 remoteVideoId: remoteVideoId,
790 });
791 } catch (err: any) {
792 let errorMessage;
793 if (err && err.message) {
794 errorMessage = err.message;
795 } else if (err) {
796 errorMessage = err;
797 } else {
798 errorMessage = '未知';
799 }
800 reject('上传视频失败,失败原因:' + errorMessage);
801 }
802 });
803 }
804
805 /**
806 * 图文作品发布
807 * @param cookies
808 * @param tokens
809 * @param imagePath
810 * @param platformSetting
811 */
812 async publishImageWorkApi(
813 cookies: string,
814 imagePath: string[],
815 platformSetting: XSLPlatformSettingType,
816 ): Promise<{
817 publishTime: number;
818 publishId: string;
819 shareLink: string;
820 }> {
821 console.log('小红书图文发布最初发布参数:', {
822 imagePath,
823 platformSetting,
824 });
825 return new Promise(async (resolve, reject) => {
826 try {
827 // 初始化cookie
828 const cookieString = CommonUtils.convertCookieToJson(cookies);
829 // 上传图片
830 const uploadImgRet = [];
831 for (const imgUrl of imagePath) {
832 // 上传图片, 获取远程Url
833 const imgRet = await this.uploadCoverFile(
834 cookieString,
835 imgUrl,
836 platformSetting.proxy,
837 );
838 // 添加到成功列表
839 uploadImgRet.push(imgRet);
840 }
841 // 获取cookie_a1
842 const cookieObject = JSON.parse(cookies);
843 let cookie_a1 = null;
844 for (const cookieItem of cookieObject) {
845 if (cookieItem.name === 'a1') {
846 cookie_a1 = cookieItem.value;
847 break;
848 }
849 }
850 // 创建作品
851 const uploadResult = {
852 imageList: uploadImgRet,
853 };
854 console.log('小红书图文发布最终发布参数:', {
855 uploadResult,
856 platformSetting,
857 });
858 const { shareLink, publishId } = (await this.postCreateVideo(
859 cookieString,
860 cookie_a1,
861 'image',
862 uploadResult,
863 platformSetting,
864 platformSetting.proxy,
865 )) as any;
866 // 返回信息
867 resolve({
868 publishTime: Math.floor(Date.now() / 1000),
869 publishId: publishId,
870 shareLink: shareLink,
871 });
872 } catch (e) {
873 reject(e);
874 }
875 });
876 }
877
878 /**
879 * 创建视频作品
880 * @param cookieString
881 * @param cookie_a1
882 * @param publishType
883 * @param uploadResult {uploadFileId, uploadCoverId, coverDimensions, fileInfo}
884 * @param platformSetting
885 * @param proxy
886 * @returns {Promise<unknown>}
887 */
888 async postCreateVideo(
889 cookieString: string,
890 cookie_a1: string,
891 publishType: 'video' | 'image',
892 uploadResult: any,
893 platformSetting: any,
894 proxy: string,
895 ) {
896 return new Promise(async (resolve, reject) => {
897 try {
898 let xhs_video_info = null;
899 let xhs_image_info = null;
900 // 如果是发布视频,则需要获取视频元信息
901 if (publishType === 'video') {
902 // 获取视频|音频元信息
903 let videoInfos = null;
904 let audioInfos = null;
905 for (const info of uploadResult.fileInfo.streams) {
906 if (
907 info.hasOwnProperty('codec_type') &&
908 info.codec_type === 'video'
909 ) {
910 videoInfos = info;
911 }
912 if (
913 info.hasOwnProperty('codec_type') &&
914 info.codec_type === 'audio'
915 ) {
916 audioInfos = info;
917 }
918 }
919 if (!videoInfos || !audioInfos) {
920 reject('创建作品失败,失败原因:获取视频、音频元信息失败!');
921 return;
922 }
923 // 拼凑视频发布参数
924 xhs_video_info = {
925 fileid: uploadResult.uploadFileId,
926 file_id: uploadResult.uploadFileId,
927 format_width: videoInfos.width,
928 format_height: videoInfos.height,
929 video_preview_type:
930 videoInfos.height > videoInfos.width
931 ? 'full_vertical_screen'
932 : '',
933 composite_metadata: {
934 video: {
935 bitrate: videoInfos.bit_rate ?? '',
936 colour_primaries: videoInfos.color_primaries ?? '',
937 duration: Math.floor(videoInfos.duration * 1000) ?? '',
938 format: videoInfos.codec_long_name.split('/')[1].trim() ?? '',
939 frame_rate: videoInfos.r_frame_rate.split('/')[0] ?? '',
940 height: videoInfos.height,
941 matrix_coefficients: videoInfos.color_primaries ?? '',
942 rotation: 0,
943 transfer_characteristics: videoInfos.color_primaries ?? '',
944 width: videoInfos.width,
945 },
946 audio: {
947 bitrate: audioInfos.bit_rate,
948 channels: audioInfos.channels,
949 duration: Math.floor(audioInfos.duration * 1000) ?? '',
950 format: audioInfos.codec_name.toUpperCase(),
951 sampling_rate: audioInfos.sample_rate,
952 },
953 },
954 timelines: [],
955 cover: {
956 fileid: uploadResult.uploadCoverId,
957 file_id: uploadResult.uploadCoverId,
958 height: uploadResult.coverDimensions.height,
959 width: uploadResult.coverDimensions.width,
960 frame: {
961 ts: 0,
962 is_user_select: false,
963 is_upload: true,
964 },
965 },
966 chapters: [],
967 chapter_sync_text: false,
968 segments: {
969 count: 1,
970 need_slice: false,
971 items: [
972 {
973 mute: 0,
974 speed: 1,
975 start: 0,
976 duration: videoInfos.duration,
977 transcoded: 0,
978 media_source: 1,
979 original_metadata: {
980 video: {
981 bitrate: videoInfos.bit_rate ?? '',
982 colour_primaries: videoInfos.color_primaries ?? '',
983 duration: Math.floor(videoInfos.duration * 1000) ?? '',
984 format:
985 videoInfos.codec_long_name.split('/')[1].trim() ?? '',
986 frame_rate: videoInfos.r_frame_rate.split('/')[0] ?? '',
987 height: videoInfos.height,
988 matrix_coefficients: videoInfos.color_primaries ?? '',
989 rotation: 0,
990 transfer_characteristics:
991 videoInfos.color_primaries ?? '',
992 width: videoInfos.width,
993 },
994 audio: {
995 bitrate: audioInfos.bit_rate,
996 channels: audioInfos.channels,
997 duration: Math.floor(audioInfos.duration * 1000) ?? '',
998 format: audioInfos.codec_name.toUpperCase(),
999 sampling_rate: audioInfos.sample_rate,
1000 },
1001 },
1002 },
1003 ],
1004 },
1005 entrance: 'web',
1006 backup_covers: [],
1007 };
1008 } else {
1009 const images = [];
1010 // 拼凑图文发布参数
1011 for (const imgInfo of uploadResult.imageList) {
1012 images.push({
1013 file_id: imgInfo.coverUploadFileId,
1014 width: imgInfo.coverDimensions.width,
1015 height: imgInfo.coverDimensions.height,
1016 metadata: {
1017 source: -1,
1018 },
1019 stickers: {
1020 version: 2,
1021 floating: [],
1022 },
1023 extra_info_json: JSON.stringify({
1024 mimeType:
1025 'image/' + imgInfo.coverDimensions.type === 'jpg'
1026 ? 'jpeg'
1027 : imgInfo.coverDimensions.type,
1028 }),
1029 });
1030 }
1031 xhs_image_info = {
1032 images: images,
1033 };
1034 }
1035 // 处理标题、@好友、话题
1036 let description = platformSetting['desc'] ?? '';
1037 const hashTag = [];
1038 if (
1039 platformSetting.hasOwnProperty('topicsDetail') &&
1040 platformSetting.topicsDetail?.length > 0
1041 ) {
1042 for (const topicInfo of platformSetting.topicsDetail) {
1043 description += ` #${topicInfo.topicName}[话题]# `;
1044 hashTag.push({
1045 id: topicInfo.topicId,
1046 name: topicInfo.topicName,
1047 link: topicInfo.topicLink,
1048 type: 'topic',
1049 });
1050 }
1051 }
1052 const ats = [];
1053 if (
1054 platformSetting.hasOwnProperty('mentionedUserInfo') &&
1055 platformSetting.mentionedUserInfo?.length > 0
1056 ) {
1057 for (const userInfo of platformSetting.mentionedUserInfo) {
1058 if (
1059 userInfo.hasOwnProperty('nickName') &&
1060 userInfo.nickName !== '' &&
1061 userInfo.hasOwnProperty('uid') &&
1062 userInfo.uid !== ''
1063 ) {
1064 description += ` @${userInfo.nickName} `;
1065 ats.push({
1066 nickname: userInfo.nickName,
1067 user_id: userInfo.uid,
1068 name: userInfo.nickName,
1069 });
1070 }
1071 }
1072 }
1073 // 处理POI
1074 let post_loc = {};
1075 if (
1076 platformSetting.hasOwnProperty('poiInfo') &&
1077 typeof platformSetting.poiInfo === 'object' &&
1078 platformSetting.poiInfo.hasOwnProperty('poiId') &&
1079 platformSetting.poiInfo.poiId !== ''
1080 ) {
1081 post_loc = {
1082 poi_id: platformSetting.poiInfo.poiId,
1083 poi_type: platformSetting.poiInfo.poiType,
1084 subname: platformSetting.poiInfo.poiAddress,
1085 name: platformSetting.poiInfo.poiName,
1086 };
1087 }
1088 // 整合请求参数
1089 const requestData = {
1090 common: {
1091 type: publishType === 'video' ? 'video' : 'normal',
1092 title: platformSetting['title'],
1093 note_id: '',
1094 desc: description,
1095 source: JSON.stringify({
1096 type: 'web',
1097 ids: '',
1098 extraInfo: JSON.stringify({
1099 subType: '',
1100 systemId: 'web',
1101 }),
1102 }),
1103 business_binds: JSON.stringify({
1104 version: 1,
1105 noteId: 0,
1106 bizType:
1107 platformSetting.hasOwnProperty('timingTime') &&
1108 platformSetting.timingTime > Date.now()
1109 ? 13
1110 : 0,
1111 noteOrderBind: {},
1112 notePostTiming: {
1113 postTime:
1114 platformSetting.hasOwnProperty('timingTime') &&
1115 platformSetting.timingTime > Date.now()
1116 ? platformSetting.timingTime.toString()
1117 : '',
1118 },
1119 noteCollectionBind: {
1120 id: '',
1121 },
1122 }),
1123 ats: ats,
1124 hash_tag: hashTag,
1125 post_loc: post_loc,
1126 privacy_info: {
1127 op_type: 1,
1128 type: platformSetting['visibility_type'],
1129 user_ids:
1130 platformSetting['visibility_type'] !== 0 ? [] : undefined,
1131 },
1132 },
1133 image_info: xhs_image_info,
1134 video_info: xhs_video_info,
1135 };
1136 // 获取加密使用的Url
1137 const encryptUrl = this.postCreateVideoUrl.replace(
1138 'https://edith.xiaohongshu.com',
1139 '',
1140 );
1141 // 逆向获取XsXt
1142 const reverseRes: any = await this.getReverseResult({
1143 url: encryptUrl,
1144 data: requestData,
1145 a1: cookie_a1,
1146 });
1147 // 发起请求
1148 const createRes = await this.makeRequest(
1149 this.postCreateVideoUrl,
1150 {
1151 method: 'POST',
1152 headers: {
1153 'Content-Type': 'application/json;charset=UTF-8',
1154 Cookie: cookieString,
1155 Referer: this.loginUrl,
1156 Origin: this.loginUrl,
1157 'X-S': reverseRes['X-s'],
1158 'X-T': reverseRes['X-t'],
1159 },
1160 data: JSON.stringify(requestData),
1161 timeout: 15000,
1162 },
1163 platformSetting.proxy,
1164 );
1165
1166 // 处理结果
1167 if (createRes.hasOwnProperty('code') && createRes.code === -1) {
1168 reject('创建作品失败,失败原因:验签未通过');
1169 return;
1170 }
1171 if (createRes.hasOwnProperty('success') && !createRes.success) {
1172 reject('创建作品失败,失败原因:' + createRes.msg || '未知');
1173 return;
1174 }
1175 if (createRes.hasOwnProperty('result') && createRes.result !== 0) {
1176 reject('创建作品失败,失败原因:' + createRes.msg || '未知');
1177 return;
1178 }
1179
1180 if (this.callback) this.callback(80, '发布完成,正在查询结果...');
1181 const worksList = await this.getWorks(cookieString);
1182 const works = worksList.data.data.notes.find(
1183 (v) => v.id === createRes.data.id,
1184 );
1185 // 返回结果
1186 resolve({
1187 shareLink: `https://www.xiaohongshu.com/explore/${createRes.data.id}?xsec_token=${works!.xsec_token}&xsec_source=${works!.xsec_source}`,
1188 publishId: createRes.data.id,
1189 });
1190 } catch (err: any) {
1191 let errorMessage;
1192 if (err && err.message) {
1193 errorMessage = err.message;
1194 } else if (err) {
1195 errorMessage = err;
1196 } else {
1197 errorMessage = '未知';
1198 }
1199 reject('创建作品失败,失败原因:' + errorMessage);
1200 }
1201 });
1202 }
1203
1204 /**
1205 * 视频作品发布
1206 * @param cookies
1207 * @param filePath
1208 * @param platformSetting
1209 * @param callback
1210 */
1211 async publishVideoWorkApi(
1212 cookies: string,
1213 filePath: string,
1214 platformSetting: XSLPlatformSettingType,
1215 callback: (progress: number, msg?: string) => void,
1216 ): Promise<{
1217 publishTime: number;
1218 publishId: string;
1219 shareLink: string;
1220 }> {
1221 console.log('小红书视频发布初始发布参数:', {
1222 filePath,
1223 platformSetting,
1224 });
1225 return new Promise(async (resolve, reject) => {
1226 try {
1227 this.callback = callback;
1228 callback(5, '正在加载...');
1229 // 获取文件信息
1230 const fileInfo = await FileUtils.getFileInfo(filePath);
1231 // 初始化cookie
1232 callback(10);
1233 const cookieString = CommonUtils.convertCookieToJson(cookies);
1234 // 获取文件大小及分片信息
1235 callback(15);
1236 const filePartInfo = await FileUtils.getFilePartInfo(
1237 filePath,
1238 this.fileBlockSize,
1239 );
1240
1241 callback(20, '正在上传视频...');
1242 // 上传视频,获取远程Url
1243 const { uploadFileId } = await this.uploadVideoFile(
1244 cookieString,
1245 filePath,
1246 filePartInfo,
1247 fileInfo,
1248 platformSetting.proxy,
1249 );
1250
1251 callback(60, '正在上传封面...');
1252 // 上传封面,获取远程Url
1253 const { coverDimensions, coverUploadFileId } =
1254 await this.uploadCoverFile(
1255 cookieString,
1256 platformSetting['cover'],
1257 platformSetting.proxy,
1258 );
1259 const cookieObject = JSON.parse(cookies);
1260 let cookie_a1 = null;
1261 for (const cookieItem of cookieObject) {
1262 if (cookieItem.name === 'a1') {
1263 cookie_a1 = cookieItem.value;
1264 break;
1265 }
1266 }
1267
1268 // 创建作品
1269 const uploadResult = {
1270 uploadFileId: uploadFileId,
1271 uploadCoverId: coverUploadFileId,
1272 coverDimensions: coverDimensions,
1273 fileInfo: fileInfo,
1274 };
1275 callback(70, '正在发布...');
1276
1277 console.log('小红书视频发布最终发布参数:', {
1278 platformSetting,
1279 uploadResult,
1280 });
1281 console.log('topicsDetail:', platformSetting.topicsDetail);
1282 const result: any = await this.postCreateVideo(
1283 cookieString,
1284 cookie_a1,
1285 'video',
1286 uploadResult,
1287 platformSetting,
1288 platformSetting.proxy,
1289 ).catch((err) => {
1290 reject(err);
1291 });
1292 console.log(result);
1293 // 返回信息
1294 resolve({
1295 publishTime: Math.floor(Date.now() / 1000),
1296 publishId: result.publishId,
1297 shareLink: result.shareLink,
1298 });
1299 } catch (err) {
1300 console.warn(err);
1301 reject(err);
1302 callback(-1);
1303 }
1304 });
1305 }
1306
1307 /**
1308 * 获取X-Sign请求参数
1309 * @param requestUrl
1310 */
1311 getRequestXSign(requestUrl: any) {
1312 return new Promise((resolve, reject) => {
1313 try {
1314 const replaceUrl =
1315 requestUrl.replace('https://www.xiaohongshu.com', '') + 'WSUDD';
1316 const xSign =
1317 'X' + crypto.createHash('md5').update(replaceUrl).digest('hex');
1318 resolve(xSign);
1319 } catch (err) {
1320 reject(err);
1321 }
1322 });
1323 }
1324
1325 /**
1326 * 获取小红书Xs|Xt
1327 */
1328 getReverseResult(args: any) {
1329 return new Promise(async (resolve, reject) => {
1330 const permitRes = await this.makeRequest(
1331 'http://116.62.154.231:7879',
1332 {
1333 method: 'POST',
1334 headers: {
1335 'Content-Type': 'application/json;charset=UTF-8',
1336 },
1337 data: args,
1338 timeout: 15000,
1339 },
1340 '',
1341 );
1342 resolve(permitRes);
1343 });
1344 }
1345
1346 // 获取话题数据
1347 async getTopics({
1348 keyword,
1349 cookies,
1350 }: {
1351 keyword: string;
1352 cookies: Electron.Cookie[];
1353 }) {
1354 return await requestNet<IXHSTopicsResponse>({
1355 url: `https://edith.xiaohongshu.com/web_api/sns/v1/search/topic`,
1356 method: 'POST',
1357 headers: {
1358 cookie: CookieToString(cookies),
1359 Referer: this.loginUrl,
1360 origin: this.loginUrl,
1361 },
1362 body: {
1363 keyword: keyword,
1364 page: {
1365 page_size: 30,
1366 page: 1,
1367 },
1368 },
1369 });
1370 }
1371
1372 // 获取位置数据
1373 async getLocations(params: {
1374 latitude: number;
1375 longitude: number;
1376 keyword: string;
1377 page?: number;
1378 size?: number;
1379 source?: string;
1380 type?: number;
1381 cookies: Electron.Cookie[];
1382 }) {
1383 return await requestNet<IXHSLocationResponse>({
1384 url: 'https://edith.xiaohongshu.com/web_api/sns/v1/local/poi/creator/search',
1385 headers: {
1386 cookie: CookieToString(params.cookies),
1387 Referer: this.loginUrl,
1388 origin: this.loginUrl,
1389 },
1390 method: 'POST',
1391 body: {
1392 ...params,
1393 page: 1,
1394 size: 50,
1395 source: 'WEB',
1396 type: 3,
1397 },
1398 });
1399 }
1400
1401 // 获取作品列表
1402 async getSearchNodeList(cookie: string, qe: string, page: number = 0) {
1403 const url = `/api/sns/web/v1/search/notes`;
1404
1405 // 生成搜索ID的函数
1406 function base36encode(number: number): string {
1407 const digits = '0123456789abcdefghijklmnopqrstuvwxyz';
1408 let base36 = '';
1409 while (number > 0) {
1410 const remainder = number % 36;
1411 base36 = digits[remainder] + base36;
1412 number = Math.floor(number / 36);
1413 }
1414 return base36;
1415 }
1416
1417 function generateSearchId(): string {
1418 const timestamp = BigInt(Date.now() * 1000) << BigInt(64);
1419 const randomValue = BigInt(Math.floor(Math.random() * 2147483646));
1420 return base36encode(Number(timestamp + randomValue));
1421 }
1422
1423 const body = {
1424 keyword: qe,
1425 page: page,
1426 page_size: 20,
1427 search_id: generateSearchId(),
1428 sort: 'general',
1429 note_type: 0,
1430 ext_flags: [],
1431 geo: '',
1432 filters: [
1433 { tags: ['general'], type: 'sort_type' },
1434 { tags: ['不限'], type: 'filter_note_type' },
1435 { tags: ['不限'], type: 'filter_note_time' },
1436 { tags: ['不限'], type: 'filter_note_range' },
1437 { tags: ['不限'], type: 'filter_pos_distance' },
1438 ],
1439 image_formats: ['jpg', 'webp', 'avif'],
1440 };
1441
1442 const reverseRes: any = await this.getReverseResult({
1443 url,
1444 a1: cookie,
1445 data: body,
1446 });
1447
1448 const res = await requestNet<any>({
1449 url: `https://edith.xiaohongshu.com${url}`,
1450 headers: {
1451 cookie: cookie,
1452 Referer: this.loginUrl,
1453 origin: this.loginUrl,
1454 'X-S': reverseRes['X-s'],
1455 'X-T': reverseRes['X-t'],
1456 userAgent:
1457 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1458 },
1459 method: 'POST',
1460 body: body,
1461 });
1462
1463 return res;
1464 }
1465
1466 // 获取作品列表
1467 async getWorks(cookie: string, page: number = 0) {
1468 const url = `/web_api/sns/v5/creator/note/user/posted?tab=0&page=${page}`;
1469 const reverseRes: any = await this.getReverseResult({
1470 url,
1471 a1: cookie,
1472 });
1473
1474 const res = await requestNet<IXHSGetWorksResponse>({
1475 url: `https://edith.xiaohongshu.com${url}`,
1476 headers: {
1477 cookie: cookie,
1478 Referer: this.loginUrl,
1479 origin: this.loginUrl,
1480 'X-S': reverseRes['X-s'],
1481 'X-T': reverseRes['X-t'],
1482 userAgent:
1483 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1484 },
1485 method: 'GET',
1486 });
1487
1488 return res;
1489 }
1490
1491 // 获取@用户列表
1492 async getUsers(cookie: Electron.Cookie[], keyword: string, page: number) {
1493 return await requestNet<XiaohongshuApiResponse>({
1494 url: `https://edith.xiaohongshu.com/web_api/sns/v1/search/user_info`,
1495 headers: {
1496 cookie: CookieToString(cookie),
1497 Referer: this.loginUrl,
1498 origin: this.loginUrl,
1499 },
1500 method: 'POST',
1501 body: {
1502 keyword,
1503 search_id: '',
1504 page: {
1505 page_size: 10,
1506 page,
1507 },
1508 },
1509 });
1510 }
1511
1512 /**
1513 * 获取评论列表
1514 * @param cookie
1515 * @param noteId
1516 * @param cursor
1517 * @returns
1518 */
1519 async getCommentList(
1520 cookie: Electron.Cookie[],
1521 note: {
1522 id: string;
1523 xsec_token: string;
1524 },
1525 cursor?: number,
1526 ) {
1527 logger.log('小红书 ------ getCommentList ---- start');
1528
1529 const url = `/api/sns/web/v2/comment/page?note_id=${note.id}&cursor=${cursor || ''}&top_comment_id=&image_formats=jpg,webp,avif&xsec_token=${note.xsec_token}`;
1530 const reverseRes: any = await this.getReverseResult({
1531 url,
1532 a1: CookieToString(cookie),
1533 });
1534
1535 const res = await requestNet<XhsCommentListResponse>({
1536 url: `https://edith.xiaohongshu.com${url}`,
1537 headers: {
1538 cookie: CookieToString(cookie),
1539 Referer: this.loginUrlHome,
1540 origin: this.loginUrlHome,
1541 'X-S': reverseRes['X-s'],
1542 'X-T': reverseRes['X-t'],
1543 userAgent:
1544 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1545 },
1546 method: 'GET',
1547 });
1548
1549 logger.log('小红书 ------ getCommentList ---- end', res);
1550
1551 return res;
1552 }
1553
1554 // 获取二级评论列表
1555 async getSecondCommentList(
1556 cookie: Electron.Cookie[],
1557 noteId: string,
1558 root_comment_id: string,
1559 cursor?: string,
1560 ) {
1561 const url = `/api/sns/web/v2/comment/sub/page?note_id=${noteId}&root_comment_id=${root_comment_id}&num=10&cursor=${cursor || ''}&top_comment_id=&image_formats=jpg,webp,avif&xsec_token=${esec_token}`;
1562 const reverseRes: any = await this.getReverseResult({
1563 url,
1564 a1: CookieToString(cookie),
1565 });
1566
1567 const res = await requestNet<XhsCommentListResponse>({
1568 url: `https://edith.xiaohongshu.com${url}`,
1569 headers: {
1570 cookie: CookieToString(cookie),
1571 Referer: this.loginUrlHome,
1572 origin: this.loginUrlHome,
1573 'X-S': reverseRes['X-s'],
1574 'X-T': reverseRes['X-t'],
1575 userAgent:
1576 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1577 },
1578 method: 'GET',
1579 });
1580
1581 return res;
1582 }
1583
1584 /**
1585 * 点赞作品
1586 * @param cookie
1587 * @param noteId
1588 * @param content
1589 * @param targetCommentId // 回复的评论ID
1590 * @returns
1591 */
1592 async likeNote(cookie: Electron.Cookie[], noteId: string) {
1593 const url = `/api/sns/web/v1/note/like`;
1594 const body = {
1595 note_oid: noteId,
1596 };
1597 const reverseRes: any = await this.getReverseResult({
1598 url,
1599 a1: CookieToString(cookie),
1600 data: body,
1601 });
1602
1603 const res = await requestNet<XhsCommentPostResponse>({
1604 url: `https://edith.xiaohongshu.com${url}`,
1605 headers: {
1606 cookie: CookieToString(cookie),
1607 Referer: 'https://www.xiaohongshu.com/',
1608 Origin: 'https://www.xiaohongshu.com',
1609 'X-S': reverseRes['X-s'],
1610 'X-T': reverseRes['X-t'],
1611 userAgent:
1612 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1613 },
1614 method: 'POST',
1615 body,
1616 });
1617
1618 return res;
1619 }
1620
1621 /**
1622 * 收藏作品
1623 * @param cookie
1624 * @param noteId
1625 * @param content
1626 * @param targetCommentId // 回复的评论ID
1627 * @returns
1628 */
1629 async shoucangNote(cookie: Electron.Cookie[], noteId: string) {
1630 const url = `/api/sns/web/v1/note/collect`;
1631 const body = {
1632 note_id: noteId,
1633 };
1634 const reverseRes: any = await this.getReverseResult({
1635 url,
1636 a1: CookieToString(cookie),
1637 data: body,
1638 });
1639
1640 const res = await requestNet<XhsCommentPostResponse>({
1641 url: `https://edith.xiaohongshu.com${url}`,
1642 headers: {
1643 cookie: CookieToString(cookie),
1644 Referer: 'https://www.xiaohongshu.com/',
1645 Origin: 'https://www.xiaohongshu.com',
1646 'X-S': reverseRes['X-s'],
1647 'X-T': reverseRes['X-t'],
1648 userAgent:
1649 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1650 },
1651 method: 'POST',
1652 body,
1653 });
1654
1655 return res;
1656 }
1657
1658 /**
1659 * 评论作品
1660 * @param cookie
1661 * @param noteId
1662 * @param content
1663 * @param targetCommentId // 回复的评论ID
1664 * @returns
1665 */
1666 async commentPost(
1667 cookie: Electron.Cookie[],
1668 noteId: string,
1669 content: string,
1670 targetCommentId?: string,
1671 ) {
1672 const url = `/api/sns/web/v1/comment/post`;
1673 const body = {
1674 note_id: noteId,
1675 content,
1676 target_comment_id: targetCommentId || undefined,
1677 at_users: [],
1678 };
1679 const reverseRes: any = await this.getReverseResult({
1680 url,
1681 a1: CookieToString(cookie),
1682 data: body,
1683 });
1684
1685 const res = await requestNet<XhsCommentPostResponse>({
1686 url: `https://edith.xiaohongshu.com${url}`,
1687 headers: {
1688 cookie: CookieToString(cookie),
1689 Referer: 'https://www.xiaohongshu.com/',
1690 Origin: 'https://www.xiaohongshu.com',
1691 'X-S': reverseRes['X-s'],
1692 'X-T': reverseRes['X-t'],
1693 userAgent:
1694 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
1695 },
1696 method: 'POST',
1697 body,
1698 });
1699 return res;
1700 }
1701 }
1702
1703 // 导出服务实例
1704 export const xiaohongshuService = new XiaohongshuService();
1705
1705 lines TYPESCRIPT