返回 AiToEarn
index.ts
1 import { BrowserWindow, screen, session } from 'electron';
2 import { CommonUtils } from '../../util/common';
3 import path from 'path';
4 import { FileUtils } from '../../util/file';
5 import * as crypto from 'crypto';
6 import { v4 as uuidv4 } from 'uuid';
7 // @ts-ignore
8 import crc32 from 'crc32';
9 import { CookieToString, getFileContent, getImageBaseInfo } from '../utils';
10 import {
11 DouyinActivityDetailResponse,
12 DouyinActivityListResponse,
13 DouyinActivityTagsResponse,
14 DouyinAllHotDataResponse,
15 DouyinCreatorCommentListResponse,
16 DouyinCreatorListResponse,
17 DouyinGetMixListResponse,
18 DouyinHotDataResponse,
19 DouyinLocationDataResponse,
20 DouyinNewCommentResponse,
21 DouyinTopicsSugResponse,
22 DouyinUserListResponse,
23 } from './douyin.type';
24 import requestNet from '../requestNet';
25 import { jsonToQueryString } from '../../util';
26 import { RetryWhile } from '../../../commont/utils';
27 import { DeclarationDouyin } from '../../../commont/plat/douyin/common.douyin';
28 import CustomFormData from 'form-data';
29
30 export type DouyinPlatformSettingType = {
31 // 自主声明
32 userDeclare?: DeclarationDouyin;
33 // 合集
34 mixInfo?: {
35 mixId: string;
36 mixName: string;
37 };
38 // 关联热点,传热点中文名称即可
39 hot_sentence?: string;
40 // 活动
41 activity?: {
42 value: string;
43 label: string;
44 }[];
45 // @用户
46 mentionedUserInfo?: {
47 nickName: string;
48 uid: string;
49 }[];
50 // 标题
51 title: string;
52 // 描述
53 caption?: string;
54 topics?: string[];
55 cover: string;
56 timingTime?: number;
57 // 0 公共 1 私密 2 好友
58 visibility_type: 0 | 1 | 2;
59 // 地址
60 poiInfo?: {
61 poiId: string;
62 poiName: string;
63 };
64 // 背景音乐
65 musicId?: string;
66 // 代理IP
67 proxyIp: string;
68 };
69
70 export class DouyinService {
71 private defaultUserAgent =
72 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
73 private loginUrl = 'https://creator.douyin.com/';
74 private getUserInfoUrl =
75 'https://creator.douyin.com/web/api/media/user/info/';
76 private getDashboardUrl =
77 'https://creator.douyin.com/janus/douyin/creator/data/overview/dashboard';
78 private getMixListUrl = 'https://creator.douyin.com/web/api/mix/list/';
79 private getSearchUserListUrl =
80 'https://creator.douyin.com/web/api/v2/discover/search/';
81 private getUploadAuthUrl =
82 'https://creator.douyin.com/web/api/media/upload/auth/v5/';
83 private getUploadVideoProofUrl = 'https://vod.bytedanceapi.com/';
84 private getCsrfTokenUrl =
85 'https://creator.douyin.com/web/api/media/anchor/search';
86 private publishUrl = 'https://creator.douyin.com/web/api/media/aweme/create/';
87 private publishUrlV2 =
88 'https://creator.douyin.com/web/api/media/aweme/create_v2/';
89 private getUploadImageProofUrl = 'https://imagex.bytedanceapi.com/';
90 private windowName = 'douyin';
91 private cookieCheckField = 'sessionid';
92 private cookieSecscdUidCheckField = 'x-web-secsdk-uid';
93 private cookieIntervalList: { [key: string]: NodeJS.Timeout } = {};
94 private windowMap: { [key: number]: BrowserWindow } = {};
95 private fileBlockSize = 3145728;
96 private app: any;
97 private callback?: (progress: number, msg?: string) => void;
98
99 /**
100 * 授权|预览
101 */
102 async loginOrView(
103 authModel: 'login' | 'view',
104 cookies?: any,
105 ): Promise<{
106 success: boolean;
107 data?: { cookie: string; userInfo: any; localStorage: string };
108 error?: string;
109 }> {
110 return new Promise((resolve, reject) => {
111 this.createAuthorizationWindow(
112 authModel === 'view' ? cookies : null,
113 ).then(async (winRes) => {
114 const { winContentsId, partition } = winRes;
115 try {
116 // 获取登录态Cookie
117 const cookies = await this.filterCookie(winContentsId, partition);
118 // 获取私钥及web_protect
119 const localStorage = await this.filterLocalStorage(winContentsId);
120 // 获取登录态用户信息
121 const userInfo = await this.getUserInfo(cookies);
122 // 返回cookie
123
124 resolve({
125 success: true,
126 data: {
127 cookie: JSON.stringify(cookies),
128 userInfo: userInfo,
129 localStorage: localStorage,
130 },
131 });
132 } catch (e) {
133 reject(e);
134 } finally {
135 // 如果是授权,则需要关闭授权窗体
136 if (authModel === 'login') {
137 const win = this.windowMap[winContentsId];
138 if (win && !win.isDestroyed()) {
139 // 移除所有事件监听器
140 win.webContents.removeAllListeners();
141 // 关闭窗口
142 win.destroy();
143 // 清理引用
144 delete this.windowMap[winContentsId];
145 win.webContents.openDevTools();
146 }
147
148 // const winBrowserWindow = BrowserWindow.fromId(winContentsId);
149 // winBrowserWindow?.close();
150 }
151 }
152 });
153 });
154 }
155
156 /**
157 * 检查用户登录是否过期
158 */
159 async checkLoginStatus(cookies: string): Promise<boolean> {
160 return new Promise(async (resolve, reject) => {
161 const cookieString = CommonUtils.convertCookieToJson(cookies);
162 try {
163 const res = await this.makeRequest(
164 this.getUserInfoUrl,
165 {
166 method: 'GET',
167 headers: {
168 Cookie: cookieString,
169 },
170 timeout: 15000,
171 },
172 '',
173 );
174
175 if (res.status_code !== 0) {
176 reject(res.status_msg ?? '未知错误');
177 return;
178 }
179 resolve(true);
180 } catch (err) {
181 console.error('检查登录状态失败:', err);
182 reject(err);
183 }
184 });
185 }
186
187 /**
188 * 数据表现
189 */
190 async getDashboardFunc(
191 cookies: string,
192 startDate?: string,
193 endDate?: string,
194 ): Promise<{
195 success: boolean;
196 data: Array<{
197 date?: string;
198 zhangfen: number;
199 bofang: number;
200 pinglun: number;
201 dianzan: number;
202 fenxiang: number;
203 zhuye: number;
204 }>;
205 res?: any;
206 }> {
207 return new Promise(async (resolve, reject) => {
208 const cookieString = CommonUtils.convertCookieToJson(cookies);
209 try {
210 const res = await this.makeRequest(
211 this.getDashboardUrl,
212 {
213 method: 'POST',
214 headers: {
215 Cookie: cookieString,
216 },
217 data: JSON.stringify({
218 recent_days: startDate && endDate ? 30 : 1,
219 }),
220 timeout: 15000,
221 },
222 '',
223 );
224
225 if (res.status_code !== 0) {
226 reject(res.status_msg ?? '未知错误');
227 return;
228 }
229
230 if (startDate && endDate) {
231 // 处理30天的数据
232 const dataMap: { [key: string]: any } = {};
233
234 // 转换日期格式为比较格式
235 const startDateStr = startDate.replace(/-/g, '');
236 const endDateStr = endDate.replace(/-/g, '');
237
238 // 遍历所有指标
239 for (const metric of res.metrics) {
240 if (metric.trends) {
241 metric.trends.forEach((trend: any) => {
242 const dateStr = trend.date_time;
243 // 判断日期是否在范围内
244 if (dateStr >= startDateStr && dateStr <= endDateStr) {
245 if (!dataMap[dateStr]) {
246 dataMap[dateStr] = {
247 date: `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`,
248 zhangfen: 0,
249 bofang: 0,
250 pinglun: 0,
251 dianzan: 0,
252 fenxiang: 0,
253 zhuye: 0,
254 };
255 }
256
257 // 根据指标名称设置对应的值
258 switch (metric.english_metric_name) {
259 case 'net_fans_cnt':
260 dataMap[dateStr].zhangfen = trend.value;
261 break;
262 case 'play_cnt':
263 dataMap[dateStr].bofang = trend.value;
264 break;
265 case 'comment_cnt':
266 dataMap[dateStr].pinglun = trend.value;
267 break;
268 case 'digg_cnt':
269 dataMap[dateStr].dianzan = trend.value;
270 break;
271 case 'share_count':
272 dataMap[dateStr].fenxiang = trend.value;
273 break;
274 case 'homepage_view_cnt':
275 dataMap[dateStr].zhuye = trend.value;
276 break;
277 }
278 }
279 });
280 }
281 }
282
283 // 转换为数组并按日期排序
284 const dataArray = Object.values(dataMap).sort((a, b) =>
285 b.date.localeCompare(a.date),
286 );
287
288 resolve({
289 success: true,
290 data: dataArray,
291 });
292 } else {
293 // 保持原有的单日数据处理逻辑
294 const data = {
295 zhangfen: 0,
296 bofang: 0,
297 pinglun: 0,
298 dianzan: 0,
299 fenxiang: 0,
300 zhuye: 0,
301 };
302
303 for (let index = 0; index < res.metrics.length; index++) {
304 const element = res.metrics[index];
305
306 if (element.english_metric_name === 'net_fans_cnt') {
307 data.zhangfen = element.metric_value;
308 }
309
310 if (element.english_metric_name === 'play_cnt') {
311 data.bofang = element.metric_value;
312 }
313
314 if (element.english_metric_name === 'comment_cnt') {
315 data.pinglun = element.metric_value;
316 }
317
318 if (element.english_metric_name === 'digg_cnt') {
319 data.dianzan = element.metric_value;
320 }
321
322 if (element.english_metric_name === 'share_count') {
323 data.fenxiang = element.metric_value;
324 }
325
326 if (element.english_metric_name === 'homepage_view_cnt') {
327 data.zhuye = element.metric_value;
328 }
329 }
330
331 resolve({
332 success: true,
333 data: [data],
334 });
335 }
336 } catch (err) {
337 console.error('检查登录状态失败:', err);
338 reject(err);
339 }
340 });
341 }
342
343 /**
344 * 获取用户信息
345 */
346 async getUserInfo(cookies: any): Promise<any> {
347 return new Promise(async (resolve, reject) => {
348 const cookieString = CommonUtils.convertCookieToJson(cookies);
349 try {
350 const res = await this.makeRequest(
351 this.getUserInfoUrl,
352 {
353 method: 'GET',
354 headers: {
355 Cookie: cookieString,
356 },
357 timeout: 15000,
358 },
359 '',
360 );
361
362 if (res.status_code === 0) {
363 resolve({
364 uid: res.user.sec_uid,
365 authorId:
366 res.user.unique_id !== '' ? res.user.unique_id : res.user.uid,
367 nickname: res.user.nickname ?? '',
368 avatar: res.user.avatar_thumb.url_list[0] ?? '',
369 fansCount: res.user.follower_count ?? 0,
370 });
371 } else {
372 reject(res.status_msg ?? '未知错误');
373 }
374 } catch (err) {
375 console.error('获取用户信息失败:', err);
376 reject(err);
377 }
378 });
379 }
380
381 /**
382 * 上传封面文件
383 */
384 private async uploadCoverFile(
385 filePath: string,
386 cookieString: string,
387 userUid: string,
388 proxy: string,
389 ): Promise<string> {
390 return new Promise(async (resolve, reject) => {
391 try {
392 // 获取上传令牌所需Ak和Tk
393 const uploadAuth = await this.getUploadAuth(cookieString);
394
395 // 获取图片数据
396 const imageRes = await getFileContent(filePath);
397 // 获取图片Crc32标识
398 const imageCrc32 = crc32(imageRes).toString(16);
399 // 获取图片上传凭证签名所需参数
400 const getUploadImageProofRequestParams = {
401 Action: 'ApplyImageUpload',
402 ServiceId: 'jm8ajry58r',
403 Version: '2018-08-01',
404 app_id: 2906,
405 s: this.generateRandomString(11),
406 user_id: userUid,
407 };
408
409 // 获取图片上传请求头
410 const requestHeadersInfo = await this.generateAuthorizationAndHeader(
411 uploadAuth.AccessKeyID,
412 uploadAuth.SecretAccessKey,
413 uploadAuth.SessionToken,
414 'cn-north-1',
415 'imagex',
416 'GET',
417 getUploadImageProofRequestParams,
418 );
419 // 获取图片上传凭证
420 const uploadImgRes = await this.makeRequest(
421 this.getUploadImageProofUrl +
422 '?' +
423 this.httpBuildQuery(getUploadImageProofRequestParams),
424 {
425 method: 'GET',
426 headers: requestHeadersInfo,
427 timeout: 15000,
428 },
429 proxy,
430 );
431
432 if (uploadImgRes['ResponseMetadata'].hasOwnProperty('Error')) {
433 reject(uploadImgRes['ResponseMetadata']['Error']['Message']);
434 return;
435 }
436
437 const UploadAddress = uploadImgRes.Result.UploadAddress;
438 // 用凭证拼接上传图片接口
439 const uploadImgUrl = `https://${UploadAddress.UploadHosts[0]}/upload/v1/${UploadAddress.StoreInfos[0].StoreUri}`;
440
441 // 上传图片
442 const imageUploadRes = await this.uploadFile(
443 uploadImgUrl,
444 imageRes,
445 {
446 Authorization: UploadAddress.StoreInfos[0].Auth,
447 'Content-Crc32': imageCrc32,
448 'Content-Type': 'application/octet-stream',
449 'X-Storage-U': userUid,
450 },
451 'POST',
452 proxy,
453 );
454
455 if (imageUploadRes.code !== 2000) {
456 reject(imageUploadRes.message);
457 return;
458 }
459
460 const commitImgParams = {
461 Action: 'CommitImageUpload',
462 ServiceId: 'jm8ajry58r',
463 Version: '2018-08-01',
464 app_id: 2906,
465 user_id: userUid,
466 };
467
468 const commitImgContent = {
469 SessionKey: UploadAddress.SessionKey,
470 };
471
472 const commitImgHead = await this.generateAuthorizationAndHeader(
473 uploadAuth.AccessKeyID,
474 uploadAuth.SecretAccessKey,
475 uploadAuth.SessionToken,
476 'cn-north-1',
477 'imagex',
478 'POST',
479 commitImgParams,
480 commitImgContent,
481 );
482
483 // 提交图片上传
484 const commitImg = await this.makeRequest(
485 this.getUploadImageProofUrl +
486 '?' +
487 this.httpBuildQuery(commitImgParams),
488 {
489 method: 'POST',
490 headers: {
491 ...commitImgHead,
492 'Content-Type': 'application/json',
493 },
494 data: JSON.stringify(commitImgContent),
495 timeout: 60000,
496 },
497 proxy,
498 );
499
500 if (commitImg['ResponseMetadata'].hasOwnProperty('Error')) {
501 reject(commitImg['ResponseMetadata']['Error']['Message']);
502 return;
503 }
504
505 resolve(commitImg.Result.Results[0].Uri);
506 } catch (err: any) {
507 console.error('上传封面文件失败:', err);
508 const errorMessage = err?.message || err || '未知';
509 reject('封面上传失败,失败原因:' + errorMessage);
510 }
511 });
512 }
513
514 /**
515 * 视频作品发布
516 */
517 async publishVideoWorkApi(
518 cookies: string,
519 tokens: any,
520 filePath: string,
521 platformSetting: DouyinPlatformSettingType,
522 callback: (progress: number, msg?: string) => void,
523 ): Promise<any> {
524 console.log('抖音开始发布视频作品,参数:', {
525 tokens,
526 platformSetting,
527 filePath,
528 });
529
530 return new Promise(async (resolve, reject) => {
531 try {
532 this.callback = callback;
533 // 初始化cookie
534 callback(5, '正在加载');
535 const cookieString = CommonUtils.convertCookieToJson(cookies);
536 callback(10);
537 // 获取用户Uid
538 const userUid = await this.getUserUid(cookieString);
539 callback(15);
540
541 // 上传封面图片 获取封面图片poster参数值
542 const poster = await this.uploadCoverFile(
543 platformSetting.cover,
544 cookieString,
545 userUid,
546 platformSetting.proxyIp,
547 );
548 callback(20);
549
550 // 上传视频 获取视频video_id参数值
551 callback(30, '正在上传视频...');
552 const videoId = await this.uploadVideo(
553 filePath,
554 cookieString,
555 userUid,
556 platformSetting.proxyIp,
557 );
558 callback(60, '视频上传完成');
559
560 // 发布视频参数
561 const publishVideoParams = this.getPublishPublicParams(platformSetting);
562 callback(65, '参数获取完成');
563
564 // 拼接视频封面内容
565 if (publishVideoParams.item) {
566 // v2处理
567 publishVideoParams.item.common.video_id = videoId;
568 publishVideoParams.item.cover.poster = poster;
569 } else {
570 // v1 处理
571 // @ts-ignore
572 publishVideoParams.video_id = videoId;
573 // @ts-ignore
574 publishVideoParams.poster = poster;
575 }
576
577 // 获取csrf-token
578 const csrfToken = await this.getSecsdkCsrfToken(cookieString);
579 callback(70, '正在发布...');
580
581 // 获取bd-ticket
582 const bdTicketHeaders = await this.getBdTicketHeaders(tokens);
583
584 console.log('抖音视频发布最终参数:', publishVideoParams);
585 // 发布视频
586 const headers = {
587 Cookie: cookieString,
588 'X-Secsdk-Csrf-Token': csrfToken,
589 ...bdTicketHeaders,
590 // 'bd-ticket-guard-web-sign-type': '1',
591 // 'bd-ticket-guard-web-version': '2',
592 };
593 // let publishResult = await requestNet({
594 // url: this.publishUrlV2,
595 // method: 'POST',
596 // headers,
597 // body: publishVideoParams,
598 // proxy: platformSetting.proxyIp,
599 // });
600 let publishResult = await this.makePublishRequest(
601 this.publishUrl,
602 {
603 url: this.publishUrl,
604 method: 'POST',
605 headers,
606 data: publishVideoParams,
607 },
608 platformSetting.proxyIp,
609 );
610 callback(100, '发布完成');
611
612 console.log('publishResult:', publishResult);
613
614 if (publishResult.status === 403 || publishResult.data === null) {
615 console.error(`发布失败,状态码403或返回数据为空`);
616 reject('请重新授权账号后发布,如多次失败,请联系技术处理!');
617 return false;
618 }
619
620 if (!publishResult.data) {
621 publishResult = {
622 // @ts-ignore
623 data: publishResult,
624 ...publishResult,
625 };
626 }
627
628 if (
629 !publishResult.data.hasOwnProperty('status_code') ||
630 publishResult.data.status_code !== 0
631 ) {
632 console.error(
633 `发布失败,状态码异常:`,
634 publishResult.data.status_code,
635 );
636 reject(publishResult.data.status_msg || '发布失败,账号可能已掉线!');
637 return false;
638 }
639
640 const item_id =
641 publishResult.data.item_id || publishResult.data.aweme.aweme_id;
642 const response = {
643 publishTime: Math.floor(Date.now() / 1000),
644 publishId: item_id,
645 shareLink: `https://www.douyin.com/user/self?from_tab_name=main&modal_id=${item_id}&showTab=post`,
646 };
647 console.log(`抖音视频发布成功,返回数据:`, response);
648 resolve(response);
649 } catch (err) {
650 console.error('发布视频过程中出现错误:', err);
651 callback(-1);
652 reject(err);
653 }
654 });
655 }
656
657 /**
658 * 图片作品发布
659 * @param cookies cookie信息
660 * @param tokens 用户令牌
661 * @param imagePath 图片路径
662 * @param platformSetting 平台设置参数
663 */
664 async publishImageWorkApi(
665 cookies: any,
666 tokens: any,
667 imagePath: string[],
668 platformSetting: DouyinPlatformSettingType,
669 ): Promise<any> {
670 console.log(`抖音开始发布图片作品,参数:`, {
671 tokens,
672 imagePath,
673 platformSetting,
674 });
675
676 return new Promise(async (resolve, reject) => {
677 try {
678 // 初始化cookie
679 const cookieString = CommonUtils.convertCookieToJson(cookies);
680
681 // 获取用户Uid
682 const userUid = await this.getUserUid(cookieString);
683
684 const images = [];
685 // 上传图片
686 for (const [index, imgUrl] of imagePath.entries()) {
687 // 上传图片 获取poster
688 const poster = await this.uploadCoverFile(
689 imgUrl,
690 cookieString,
691 userUid,
692 platformSetting.proxyIp,
693 );
694
695 // 获取图片信息
696 const imageBaseInfo = await getImageBaseInfo(imgUrl);
697
698 images.push({
699 uri: poster,
700 width: imageBaseInfo.width,
701 height: imageBaseInfo.height,
702 });
703 }
704
705 // 获取公共请求参数
706 const publishImgParams = this.getPublishPublicParams(platformSetting);
707 // 拼接图文内容
708 publishImgParams.images = images;
709
710 // 获取csrf-token
711 const csrfToken = await this.getSecsdkCsrfToken(cookieString);
712
713 // 获取bd-ticket
714 const bdTicketHeaders = await this.getBdTicketHeaders(tokens);
715
716 console.log('抖音图文发布最终参数:', publishImgParams);
717 // 发布图文
718 const publishResult = await this.makePublishRequest(
719 this.publishUrl,
720 {
721 method: 'POST',
722 headers: {
723 Cookie: cookieString,
724 'X-Secsdk-Csrf-Token': csrfToken,
725 ...bdTicketHeaders,
726 },
727 data: publishImgParams,
728 },
729 platformSetting.proxyIp,
730 );
731
732 if (publishResult.status === 403 || publishResult.data === null) {
733 console.error(`发布失败,状态码403或返回数据为空`);
734 reject('请重新授权账号后发布,如多次失败,请联系技术处理!');
735 return false;
736 }
737
738 if (
739 !publishResult.hasOwnProperty('status_code') ||
740 publishResult.status_code !== 0
741 ) {
742 console.error(`发布失败,状态码异常:`, publishResult.status_code);
743 reject(publishResult.status_msg || '发布失败,账号可能已掉线!');
744 return false;
745 }
746
747 const response = {
748 publishTime: Math.floor(Date.now() / 1000),
749 publishId: publishResult.aweme.aweme_id,
750 shareLink: `https://www.douyin.com/user/self?from_tab_name=main&modal_id=${publishResult.aweme.aweme_id}&showTab=post`,
751 };
752 console.log(`发布成功,返回数据:`, response);
753 resolve(response);
754 } catch (err) {
755 console.error(`发布图片作品失败:`, err);
756 reject(err);
757 }
758 });
759 }
760
761 /**
762 * 创建授权窗口
763 */
764 private async createAuthorizationWindow(cookies: any = null) {
765 return new Promise<{ winContentsId: number; partition: string }>(
766 async (resolve) => {
767 // 生成随机partition
768 const partition = Date.now().toString();
769
770 // 获取屏幕尺寸
771 const { width, height } = screen.getPrimaryDisplay().workAreaSize;
772
773 // 创建窗口
774 const win = new BrowserWindow({
775 width: Math.ceil(width * 0.9),
776 height: Math.ceil(height * 0.8),
777 show: false,
778 icon: path.join(process.cwd(), 'public', 'images', 'logo-32.png'),
779 webPreferences: {
780 contextIsolation: false,
781 nodeIntegration: false,
782 partition: partition,
783 },
784 });
785 win.show();
786
787 const winContentsId = win.webContents.id;
788 // 存储窗口引用
789 this.windowMap[winContentsId] = win;
790
791 // 如果是查看并且有cookie传入,如果有,判断登录态,如果登录态还在,则设置cookie
792 if (cookies) {
793 try {
794 const loginStatus = await this.checkLoginStatus(cookies);
795 if (loginStatus) {
796 const cookiesObj =
797 typeof cookies === 'string' ? JSON.parse(cookies) : cookies;
798 for (const cookie of cookiesObj) {
799 await session.fromPartition(partition).cookies.set({
800 url: this.loginUrl,
801 name: cookie.name,
802 value: cookie.value,
803 domain: cookie.domain,
804 path: cookie.path,
805 });
806 }
807 }
808 } catch (err) {
809 console.error('Set cookies error:', err);
810 }
811 }
812
813 // 载入URL及设置userAgent
814 win.loadURL(this.loginUrl, {
815 userAgent: this.defaultUserAgent,
816 });
817
818 // 显示页面并设置置顶
819 win.once('ready-to-show', () => {
820 win.focus();
821 win.center();
822 win.setAlwaysOnTop(true);
823 win.setAlwaysOnTop(false);
824 });
825
826 // 监听窗口销毁
827 win.webContents.on('destroyed', () => {
828 if (this.cookieIntervalList.hasOwnProperty(winContentsId)) {
829 clearInterval(this.cookieIntervalList[winContentsId]);
830 delete this.cookieIntervalList[winContentsId];
831 }
832 delete this.windowMap[winContentsId];
833 });
834
835 // 监听窗口加载完成
836 win.webContents.on('did-finish-load', async () => {
837 if (win.webContents.getURL() === this.loginUrl) {
838 let checkEleNum = 0;
839 while (true) {
840 if (checkEleNum >= 10) break;
841 if (!win || win.isDestroyed()) break;
842
843 const hasEle = await win.webContents.executeJavaScript(`
844 (function() {
845 return document.querySelector('.dux-icon-14') !== null;
846 })()
847 `);
848
849 if (hasEle) {
850 await win.webContents.executeJavaScript(`
851 (function() {
852 document.querySelector('.dux-icon-14').parentElement.parentElement.style.display = 'none'
853 })()
854 `);
855 break;
856 }
857 checkEleNum++;
858 await CommonUtils.waitFor(1000);
859 }
860 }
861 resolve({ winContentsId, partition });
862 });
863 },
864 );
865 }
866
867 /**
868 * 通用请求方法
869 */
870 private async makeRequest(
871 url: string,
872 options: any,
873 proxy: string,
874 ): Promise<any> {
875 return new Promise(async (resolve, reject) => {
876 const res = await requestNet({
877 method: options.method,
878 url: url,
879 headers: options.headers,
880 body: options.data,
881 proxy,
882 });
883 resolve(res.data);
884 });
885 }
886
887 /**
888 * 上传文件到远程服务器
889 * @param url 上传地址
890 * @param fileContent 文件内容
891 * @param headers 请求头
892 * @param method HTTP 方法
893 * @param proxy
894 */
895 private async uploadFile(
896 url: string,
897 fileContent: Buffer,
898 headers: any,
899 method: string = 'PUT',
900 proxy: string,
901 ): Promise<any> {
902 return new Promise(async (resolve, reject) => {
903 const res = await requestNet({
904 method: method as 'POST',
905 url: url,
906 headers: headers,
907 body: fileContent,
908 isFile: true,
909 proxy,
910 });
911 console.log(res);
912 resolve(res.data);
913 });
914 }
915
916 /**
917 * 获取上传凭证所需Ak和Tk
918 */
919 private async getUploadAuth(cookieString: string): Promise<any> {
920 return new Promise(async (resolve, reject) => {
921 try {
922 const authRes = await this.makeRequest(
923 this.getUploadAuthUrl,
924 {
925 method: 'GET',
926 headers: {
927 Cookie: cookieString,
928 },
929 timeout: 15000,
930 },
931 '',
932 );
933
934 if (
935 !authRes.hasOwnProperty('status_code') ||
936 authRes.status_code !== 0
937 ) {
938 reject(authRes.status_msg ?? '获取上传凭证失败,账号可能已掉线!');
939 return;
940 }
941 resolve(JSON.parse(authRes.auth));
942 } catch (err) {
943 console.error('获取上传凭证失败:', err);
944 reject(err);
945 }
946 });
947 }
948
949 /**
950 * 生成请求所需Header
951 */
952 private async generateAuthorizationAndHeader(
953 accessKeyID: string,
954 secretAccessKey: string,
955 sessionToken: string,
956 region: string,
957 service: string,
958 requestMethod: string,
959 requestParams: any,
960 requestBody: any = {},
961 ): Promise<any> {
962 return new Promise((resolve) => {
963 // 获取当前ISO时间
964 const now = new Date();
965 const amzDate = now.toISOString().replace(/[-:]/g, '').slice(0, 15) + 'Z';
966
967 // 生成请求的Header
968 const requestHeaders = this.addHeaders(
969 amzDate,
970 sessionToken,
971 requestBody,
972 );
973
974 // 生成请求的Authorization
975 const authorizationParams = [
976 'AWS4-HMAC-SHA256 Credential=' +
977 accessKeyID +
978 '/' +
979 this.credentialString(amzDate, region, service),
980 'SignedHeaders=' + this.signedHeaders(requestHeaders),
981 'Signature=' +
982 this.signature(
983 secretAccessKey,
984 amzDate,
985 region,
986 service,
987 requestMethod,
988 requestParams,
989 requestHeaders,
990 requestBody,
991 ),
992 ];
993 const authorization = authorizationParams.join(', ');
994
995 // 返回Headers
996 const headers: any = {};
997 for (const key in requestHeaders) {
998 headers[key] = requestHeaders[key];
999 }
1000 headers['Authorization'] = authorization;
1001 resolve(headers);
1002 });
1003 }
1004
1005 /**
1006 * 生成请求所需Header
1007 */
1008 private addHeaders(
1009 amzDate: string,
1010 sessionToken: string,
1011 requestBody: any,
1012 ): any {
1013 const headers = {
1014 'X-Amz-Date': amzDate,
1015 'X-Amz-Security-Token': sessionToken,
1016 };
1017 if (Object.keys(requestBody).length > 0) {
1018 // @ts-ignore
1019 headers['X-Amz-Content-Sha256'] = crypto
1020 .createHash('sha256')
1021 .update(JSON.stringify(requestBody))
1022 .digest('hex');
1023 }
1024 return headers;
1025 }
1026
1027 /**
1028 * 获取credentialString
1029 */
1030 private credentialString(
1031 amzDate: string,
1032 region: string,
1033 service: string,
1034 ): string {
1035 const credentialArr = [
1036 amzDate.substring(0, 8),
1037 region,
1038 service,
1039 'aws4_request',
1040 ];
1041 return credentialArr.join('/');
1042 }
1043
1044 /**
1045 * 生成随机数+字母
1046 */
1047 private generateRandomString(length: number): string {
1048 let result = '';
1049 const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
1050 const charactersLength = characters.length;
1051 for (let i = 0; i < length; i++) {
1052 result += characters.charAt(Math.floor(Math.random() * charactersLength));
1053 }
1054 return result;
1055 }
1056
1057 /**
1058 * 获取signedHeaders
1059 */
1060 private signedHeaders(requestHeaders: any): string {
1061 const headers: string[] = [];
1062 Object.keys(requestHeaders).forEach(function (r) {
1063 r = r.toLowerCase();
1064 headers.push(r);
1065 });
1066 return headers.sort().join(';');
1067 }
1068
1069 /**
1070 * 生成canonicalString
1071 */
1072 private canonicalString(
1073 requestMethod: string,
1074 requestParams: any,
1075 requestHeaders: any,
1076 requestBody: any,
1077 ): string {
1078 let canonicalHeaders: string[] = [];
1079 const headerKeys = Object.keys(requestHeaders).sort();
1080 for (let i = 0; i < headerKeys.length; i++) {
1081 canonicalHeaders.push(
1082 headerKeys[i].toLowerCase() + ':' + requestHeaders[headerKeys[i]],
1083 );
1084 }
1085 // @ts-ignore
1086 canonicalHeaders = canonicalHeaders.join('\n') + '\n';
1087
1088 let body = '';
1089 if (Object.keys(requestBody).length > 0) {
1090 body = JSON.stringify(requestBody);
1091 }
1092
1093 const canonicalStringArr = [
1094 requestMethod.toUpperCase(),
1095 '/',
1096 this.httpBuildQuery(requestParams),
1097 canonicalHeaders,
1098 this.signedHeaders(requestHeaders),
1099 crypto.createHash('sha256').update(body).digest('hex'),
1100 ];
1101 return canonicalStringArr.join('\n');
1102 }
1103
1104 /**
1105 * 获取signature
1106 */
1107 private signature(
1108 secretAccessKey: string,
1109 amzDate: string,
1110 region: string,
1111 service: string,
1112 requestMethod: string,
1113 requestParams: any,
1114 requestHeaders: any,
1115 requestBody: any,
1116 ): string {
1117 // 生成signingKey
1118 const amzDay = amzDate.substring(0, 8);
1119 const kDate = crypto
1120 .createHmac('sha256', 'AWS4' + secretAccessKey)
1121 .update(amzDay)
1122 .digest();
1123 const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
1124 const kService = crypto
1125 .createHmac('sha256', kRegion)
1126 .update(service)
1127 .digest();
1128 const signingKey = crypto
1129 .createHmac('sha256', kService)
1130 .update('aws4_request')
1131 .digest();
1132
1133 // 生成StringToSign
1134 const stringToSignArr = [
1135 'AWS4-HMAC-SHA256',
1136 amzDate,
1137 this.credentialString(amzDate, region, service),
1138 crypto
1139 .createHash('sha256')
1140 .update(
1141 this.canonicalString(
1142 requestMethod,
1143 requestParams,
1144 requestHeaders,
1145 requestBody,
1146 ),
1147 )
1148 .digest('hex'),
1149 ];
1150 const stringToSign = stringToSignArr.join('\n');
1151 return crypto
1152 .createHmac('sha256', signingKey)
1153 .update(stringToSign)
1154 .digest('hex');
1155 }
1156
1157 /**
1158 * 生成http请求参数字符串
1159 */
1160 private httpBuildQuery(params: any): string {
1161 const searchParams = new URLSearchParams();
1162 for (const key in params) {
1163 if (params.hasOwnProperty(key)) {
1164 searchParams.append(key, params[key]);
1165 }
1166 }
1167 return searchParams.toString();
1168 }
1169
1170 /**
1171 * 获取网站登录cookie
1172 */
1173 private async filterCookie(
1174 winContentsId: number,
1175 partition: string,
1176 ): Promise<Electron.Cookie[]> {
1177 return new Promise((resolve, reject) => {
1178 // 监听cookie状态
1179 this.cookieIntervalList[winContentsId] = setInterval(async () => {
1180 try {
1181 const cookies = await session
1182 .fromPartition(partition)
1183 .cookies.get({});
1184
1185 const alreadyLogin = cookies.some((item) =>
1186 item.name.includes(this.cookieCheckField),
1187 );
1188 const hasSecUid = cookies.some((item) =>
1189 item.name.includes(this.cookieSecscdUidCheckField),
1190 );
1191
1192 // 如果获取到登录状态cookie
1193 if (alreadyLogin && hasSecUid) {
1194 // 关闭定时器
1195 if (this.cookieIntervalList.hasOwnProperty(winContentsId)) {
1196 clearInterval(this.cookieIntervalList[winContentsId]);
1197 delete this.cookieIntervalList[winContentsId];
1198 }
1199 // 返回Cookie信息
1200 resolve(cookies);
1201 }
1202 } catch (err) {
1203 console.error('获取cookie失败:', err);
1204 reject('获取网站cookie失败');
1205 }
1206 }, 3000);
1207 });
1208 }
1209
1210 /**
1211 * 获取私钥等信息
1212 */
1213 private async filterLocalStorage(winContentsId: number): Promise<string> {
1214 return new Promise(async (resolve, reject) => {
1215 try {
1216 // 先从 windowMap 中获取窗口
1217 let win = this.windowMap[winContentsId];
1218
1219 // 如果 windowMap 中没有,尝试从 BrowserWindow 获取
1220 if (!win) {
1221 // @ts-ignore
1222 win = BrowserWindow.fromId(winContentsId);
1223 }
1224
1225 // 如果仍然找不到窗口
1226 if (!win || win.isDestroyed()) {
1227 console.error('找不到窗口或窗口已销毁, ID:', winContentsId);
1228 reject('找不到有效的窗口');
1229 return;
1230 }
1231
1232 let retryCount = 0;
1233 const maxRetries = 10; // 最大重试次数
1234
1235 while (retryCount < maxRetries) {
1236 if (!win || win.isDestroyed()) {
1237 console.error('窗口已被销毁,停止获取 localStorage');
1238 reject('窗口已被销毁');
1239 break;
1240 }
1241
1242 try {
1243 const hasLocalStorage = await win.webContents.executeJavaScript(`
1244 (function() {
1245 return window.localStorage['security-sdk/s_sdk_crypt_sdk'] !== undefined &&
1246 window.localStorage['security-sdk/s_sdk_sign_data_key/web_protect'] !== undefined;
1247 })()
1248 `);
1249
1250 if (hasLocalStorage) {
1251 const privateKey = await win.webContents.executeJavaScript(`
1252 (function() {
1253 try {
1254 const sdkData = window.localStorage['security-sdk/s_sdk_crypt_sdk'];
1255 const parsedData = JSON.parse(sdkData);
1256 const parsedInnerData = JSON.parse(parsedData.data);
1257 return parsedInnerData.ec_privateKey;
1258 } catch (e) {
1259 console.error('解析 privateKey 失败:', e);
1260 return null;
1261 }
1262 })()
1263 `);
1264
1265 const webProtect = await win.webContents.executeJavaScript(`
1266 (function() {
1267 try {
1268 const protectData = window.localStorage['security-sdk/s_sdk_sign_data_key/web_protect'];
1269 const parsedData = JSON.parse(protectData);
1270 return parsedData.data;
1271 } catch (e) {
1272 console.error('解析 webProtect 失败:', e);
1273 return null;
1274 }
1275 })()
1276 `);
1277
1278 if (!privateKey || !webProtect) {
1279 throw new Error('获取到的 privateKey 或 webProtect 为空');
1280 }
1281
1282 resolve(
1283 JSON.stringify({
1284 privateKey: privateKey,
1285 webProtect: webProtect,
1286 }),
1287 );
1288 break;
1289 }
1290 } catch (err) {
1291 console.error(
1292 `第 ${retryCount + 1} 次尝试获取 localStorage 失败:`,
1293 err,
1294 );
1295 }
1296
1297 retryCount++;
1298 await CommonUtils.waitFor(1000);
1299 }
1300
1301 if (retryCount >= maxRetries) {
1302 reject('获取 localStorage 超过最大重试次数');
1303 }
1304 } catch (err) {
1305 console.error('获取 localStorage 过程出错:', err);
1306 reject(err instanceof Error ? err.message : '获取 localStorage 失败');
1307 }
1308 });
1309 }
1310
1311 /**
1312 * 获取用户Uid
1313 * @param cookieString
1314 */
1315 private async getUserUid(cookieString: string): Promise<string> {
1316 return new Promise(async (resolve, reject) => {
1317 try {
1318 const userInfoRes = await this.makeRequest(
1319 this.getUserInfoUrl,
1320 {
1321 method: 'GET',
1322 headers: {
1323 Cookie: cookieString,
1324 },
1325 dataType: 'json',
1326 timeout: 15000,
1327 },
1328 '',
1329 );
1330
1331 if (
1332 !userInfoRes.hasOwnProperty('status_code') ||
1333 userInfoRes.status_code !== 0
1334 ) {
1335 console.error('获取用户Uid失败:', userInfoRes.status_msg);
1336 reject(userInfoRes.status_msg ?? '获取用户Uid失败,账号可能已掉线!');
1337 return false;
1338 }
1339
1340 resolve(userInfoRes.user.uid);
1341 } catch (err) {
1342 console.error('获取用户Uid过程出错:', err);
1343 reject(err);
1344 }
1345 });
1346 }
1347
1348 /**
1349 * 上传视频
1350 * @param filePath 视频文件路径
1351 * @param cookieString cookie字符串
1352 * @param userUid 用户ID
1353 * @param proxy
1354 */
1355 private async uploadVideo(
1356 filePath: string,
1357 cookieString: string,
1358 userUid: string,
1359 proxy: string,
1360 ): Promise<string> {
1361 return new Promise(async (resolve, reject) => {
1362 try {
1363 // 获取文件大小及分片信息
1364 const filePartInfo = await FileUtils.getFilePartInfo(
1365 filePath,
1366 this.fileBlockSize,
1367 );
1368
1369 // 获取上传令牌所需Ak和Tk
1370 const uploadAuth = await this.getUploadAuth(cookieString);
1371
1372 // 获取视频上传凭证签名所需参数
1373 const getUploadVideoProofRequestParams = {
1374 Action: 'ApplyUploadInner',
1375 FileSize: filePartInfo.fileSize,
1376 FileType: 'video',
1377 IsInner: 1,
1378 SpaceName: 'aweme',
1379 Version: '2020-11-19',
1380 app_id: 2906,
1381 s: this.generateRandomString(11),
1382 user_id: userUid.toString(),
1383 };
1384
1385 // 获取视频上传凭证签名
1386 const requestHeadersInfo = await this.generateAuthorizationAndHeader(
1387 uploadAuth.AccessKeyID,
1388 uploadAuth.SecretAccessKey,
1389 uploadAuth.SessionToken,
1390 'cn-north-1',
1391 'vod',
1392 'GET',
1393 getUploadVideoProofRequestParams,
1394 );
1395
1396 // 获取视频上传凭证
1397 const uploadRes = await this.makeRequest(
1398 this.getUploadVideoProofUrl +
1399 '?' +
1400 this.httpBuildQuery(getUploadVideoProofRequestParams),
1401 {
1402 method: 'GET',
1403 headers: requestHeadersInfo,
1404 dataType: 'json',
1405 timeout: 15000,
1406 },
1407 proxy,
1408 );
1409
1410 if (uploadRes['ResponseMetadata'].hasOwnProperty('Error')) {
1411 reject(uploadRes['ResponseMetadata']['Error']['Message']);
1412 return false;
1413 }
1414
1415 // 获取上传节点信息
1416 const uploadNodes = uploadRes.Result.InnerUploadAddress.UploadNodes[0];
1417 const storeUri = uploadNodes.StoreInfos[0].StoreUri;
1418 const uploadUrl = `https://${uploadNodes.UploadHost}/upload/v1/${storeUri}`;
1419
1420 // 生成UploadUuid
1421 const uploadUuId = uuidv4();
1422
1423 // 公共上传参数
1424 const uploadParams = {
1425 uploadid: uploadUuId,
1426 uploadmode: null,
1427 phase: 'transfer',
1428 };
1429
1430 // 上传分片数据
1431 const uploadBlockRes: string[] = [];
1432 console.log('开始分片上传...');
1433
1434 // 开始上传
1435 for (const i in filePartInfo.blockInfo) {
1436 if (this.callback)
1437 this.callback(
1438 50,
1439 `上传视频(${i}/${filePartInfo.blockInfo.length})`,
1440 );
1441 let errorMsg = '';
1442 const isSuccess = await RetryWhile(async () => {
1443 // 设置当前文件分片信息
1444 const nowUploadParams = JSON.parse(JSON.stringify(uploadParams));
1445 nowUploadParams['part_number'] = parseInt(i) + 1;
1446 nowUploadParams['part_offset'] = parseInt(i) * this.fileBlockSize;
1447
1448 // 获取分片内容
1449 const chunkStart =
1450 i === '0' ? 0 : filePartInfo.blockInfo[parseInt(i) - 1];
1451 const chunkEnd = filePartInfo.blockInfo[i] - 1;
1452 const chunkContent = await FileUtils.getFilePartContent(
1453 filePath,
1454 chunkStart,
1455 chunkEnd,
1456 );
1457
1458 // 获取文件crc32
1459 const fileCrc32 = crc32(chunkContent).toString(16);
1460
1461 // 执行分片上传
1462 const uploadPartRes = await this.uploadFile(
1463 uploadUrl + '?' + this.httpBuildQuery(nowUploadParams),
1464 chunkContent,
1465 {
1466 Authorization: uploadNodes.StoreInfos[0].Auth,
1467 'Content-Crc32': fileCrc32,
1468 'Content-Type': 'application/octet-stream',
1469 'X-Logical-Part-Mode':
1470 uploadNodes.UploadHeader['X-Logical-Part-Mode'],
1471 'X-Storage-U': userUid,
1472 },
1473 'POST',
1474 proxy,
1475 );
1476
1477 if (uploadPartRes === null) {
1478 errorMsg = '上传文件失败';
1479 return false;
1480 }
1481
1482 if (uploadPartRes.code !== 2000) {
1483 errorMsg = uploadPartRes.message;
1484 return false;
1485 }
1486 // 上传分片成功
1487 uploadBlockRes.push(
1488 `${uploadPartRes.data.part_number}:${uploadPartRes.data.crc32}`,
1489 );
1490 return true;
1491 }, 3);
1492
1493 if (!isSuccess) {
1494 reject(errorMsg);
1495 break;
1496 }
1497 }
1498
1499 // 分片上传完成,合并分片
1500 console.log('所有分片上传完成,开始合并...');
1501 const uploadBlockResStr = uploadBlockRes.join(',');
1502 uploadParams.phase = 'finish';
1503 // @ts-ignore
1504 uploadParams.uploadmode = 'part';
1505
1506 // 请求分片合成
1507 const uploadFinishRes = await this.uploadFile(
1508 uploadUrl + '?' + this.httpBuildQuery(uploadParams),
1509 Buffer.from(uploadBlockResStr),
1510 {
1511 Authorization: uploadNodes.StoreInfos[0].Auth,
1512 'Content-Type': 'text/plain;charset=UTF-8',
1513 'X-Logical-Part-Mode':
1514 uploadNodes.UploadHeader['X-Logical-Part-Mode'],
1515 'X-Storage-U': userUid,
1516 },
1517 'POST',
1518 proxy,
1519 );
1520 console.log('分片合并结果:', uploadFinishRes);
1521
1522 if (uploadFinishRes === null) {
1523 reject('合成文件分片失败');
1524 return false;
1525 }
1526
1527 if (uploadFinishRes.code !== 2000) {
1528 reject(uploadFinishRes.message);
1529 return false;
1530 }
1531
1532 // 分片合并完成,提交CommitUploadInner
1533 const commitRequestParams = {
1534 Action: 'CommitUploadInner',
1535 SpaceName: 'aweme',
1536 Version: '2020-11-19',
1537 app_id: 2906,
1538 user_id: userUid,
1539 };
1540
1541 const commitRequestBody = {
1542 SessionKey: uploadNodes.SessionKey,
1543 Functions: [],
1544 };
1545
1546 // 生成请求令牌
1547 const commitRequestHeadersInfo =
1548 await this.generateAuthorizationAndHeader(
1549 uploadAuth.AccessKeyID,
1550 uploadAuth.SecretAccessKey,
1551 uploadAuth.SessionToken,
1552 'cn-north-1',
1553 'vod',
1554 'POST',
1555 commitRequestParams,
1556 commitRequestBody,
1557 );
1558
1559 // 请求CommitUploadInner
1560 const commitRes = await this.makeRequest(
1561 this.getUploadVideoProofUrl +
1562 '?' +
1563 this.httpBuildQuery(commitRequestParams),
1564 {
1565 method: 'POST',
1566 headers: commitRequestHeadersInfo,
1567 data: JSON.stringify(commitRequestBody),
1568 dataType: 'json',
1569 timeout: 60000,
1570 },
1571 proxy,
1572 );
1573 console.log('提交上传结果:', commitRes);
1574
1575 if (commitRes['ResponseMetadata'].hasOwnProperty('Error')) {
1576 reject(commitRes['ResponseMetadata']['Error']['Message']);
1577 return false;
1578 }
1579
1580 console.log('视频上传成功,视频ID:', commitRes.Result.Results[0].Vid);
1581 resolve(commitRes.Result.Results[0].Vid);
1582 } catch (err) {
1583 console.error('视频上传过程出错:', err);
1584 let errorMessage;
1585 // @ts-ignore
1586 if (err && err.message) {
1587 // @ts-ignore
1588 errorMessage = err.message;
1589 } else if (err) {
1590 errorMessage = err;
1591 } else {
1592 errorMessage = '未知';
1593 }
1594 reject('视频上传失败,失败原因:' + errorMessage);
1595 }
1596 });
1597 }
1598
1599 // 发布视频、v2 api参数处理
1600 private getPublishPublicParamsV2(platformSetting: DouyinPlatformSettingType) {
1601 const parmasDisposeOK = this.getPublishPublicParams(platformSetting);
1602
1603 return {
1604 item: {
1605 anchor: parmasDisposeOK.hasOwnProperty('poi_id')
1606 ? {
1607 anchor_content: JSON.stringify({
1608 is_commerce_intention: false,
1609 }),
1610 poi_id: parmasDisposeOK.poi_id,
1611 poi_name: parmasDisposeOK.poi_name,
1612 }
1613 : {},
1614 assistant: {
1615 is_post_assistant: 1,
1616 is_preview: 0,
1617 },
1618 chapter: {
1619 chapter: JSON.stringify({
1620 chapter_abstract: '',
1621 chapter_details: [],
1622 chapter_type: 0,
1623 chapter_tools_info: {
1624 chapter_recommend_detail: [],
1625 chapter_recommend_abstract: '',
1626 chapter_source: 2,
1627 chapter_recommend_type: -2,
1628 create_date: 1741767807,
1629 is_pc: '1',
1630 is_pre_generated: '0',
1631 is_syn: '1',
1632 },
1633 }),
1634 },
1635 common: {
1636 // 活动
1637 activity: parmasDisposeOK.activity,
1638 // 挑战
1639 challenges: parmasDisposeOK.activity,
1640 text: parmasDisposeOK.text,
1641 item_title: parmasDisposeOK.item_title,
1642 text_extra: JSON.stringify(parmasDisposeOK.text_extra),
1643 creation_id: '',
1644 // 是否允许下载
1645 download: 1,
1646 hashtag_source: 'recommend/recommend/recommend',
1647 // 热点中文
1648 hot_sentence: platformSetting.hot_sentence,
1649 // 标题
1650 media_type: 4,
1651 mentions: JSON.stringify(parmasDisposeOK.mentions),
1652 music_id: '',
1653 video_id: '',
1654 music_source: 0,
1655 // 定时,
1656 timing: parmasDisposeOK.timing,
1657 // 可见性
1658 visibility_type: platformSetting['visibility_type'],
1659 },
1660 cover: {
1661 cover_text: null,
1662 cover_text_uri: null,
1663 cover_tools_extend_info: '{}',
1664 cover_tools_info: JSON.stringify({}),
1665 poster: '',
1666 poster_delay: 0,
1667 },
1668 declare: platformSetting.userDeclare
1669 ? {
1670 user_declare_info: JSON.stringify({
1671 // 内容由AI生成
1672 choose_value: platformSetting.userDeclare,
1673 }),
1674 }
1675 : {},
1676 // 合集
1677 mix: {
1678 mix_id: parmasDisposeOK['mix_id'],
1679 },
1680 open_platform: {},
1681 sync: {
1682 should_sync: false,
1683 sync_to_toutiao: 0,
1684 },
1685 },
1686 };
1687 }
1688
1689 /**
1690 * 获取视频|图文公共发布参数
1691 * @param platformSetting 平台设置参数
1692 */
1693 private getPublishPublicParams(
1694 platformSetting: DouyinPlatformSettingType,
1695 ): any {
1696 // 处理描述
1697 let text = `${platformSetting['title'] || ''} ${platformSetting['caption'] || ''}`;
1698 // 标题扩展属性
1699 const textExtra = [];
1700 // @好友参数
1701 const mentions = [];
1702
1703 // 处理话题
1704 if (
1705 (platformSetting.topics && platformSetting.topics?.length > 0) ||
1706 (platformSetting.activity && platformSetting.activity?.length > 0)
1707 ) {
1708 for (const topic of [
1709 ...(platformSetting.topics || []),
1710 ...(platformSetting.activity?.map((v) => v.label) || []),
1711 ]) {
1712 // 扩展属性追加话题位置
1713 const extraItem = {
1714 start: text.length,
1715 type: 1,
1716 hashtag_name: topic,
1717 hashtag_id: 0,
1718 user_id: '',
1719 caption_start: 0,
1720 caption_end: 0,
1721 };
1722 // 标题追加话题
1723 text += `#${topic} `;
1724 // 计算话题结束位置
1725 // @ts-ignore
1726 extraItem.end = text.length - 1;
1727 // 追加到扩展属性
1728 textExtra.push(extraItem);
1729 }
1730 }
1731
1732 // 处理@好友
1733 if (
1734 platformSetting.hasOwnProperty('mentionedUserInfo') &&
1735 platformSetting.mentionedUserInfo &&
1736 platformSetting.mentionedUserInfo?.length > 0
1737 ) {
1738 for (const userInfo of platformSetting.mentionedUserInfo) {
1739 if (userInfo.hasOwnProperty('nickName') && userInfo.nickName !== '') {
1740 // 扩展属性追加@好友位置
1741 const extraItem = {
1742 start: text.length,
1743 type: 0,
1744 hashtag_name: '',
1745 hashtag_id: 0,
1746 user_id: userInfo.uid,
1747 caption_start: 0,
1748 caption_end: 0,
1749 };
1750 // 标题追加@好友
1751 text += ` @${userInfo.nickName}`;
1752 // 计算话题结束位置
1753 // @ts-ignore
1754 extraItem.end = text.length;
1755 // 追加到扩展属性
1756 textExtra.push(extraItem);
1757 // 追加到@好友
1758 mentions.push(userInfo.uid);
1759 }
1760 }
1761 }
1762
1763 // 整合发布参数
1764 const publishParams = {
1765 hot_sentence: platformSetting.hot_sentence,
1766 user_declare_info: platformSetting.userDeclare
1767 ? {
1768 choose_value: platformSetting.userDeclare,
1769 }
1770 : {},
1771 item_title: platformSetting['title'] ?? '',
1772 text,
1773 text_extra: textExtra,
1774 mentions: mentions,
1775 // 0 公共 1 私密 2 好友
1776 visibility_type: platformSetting['visibility_type'],
1777 download: 1,
1778 activity: JSON.stringify(
1779 platformSetting.activity?.map((v) => v.value) || [],
1780 ),
1781 };
1782
1783 // 处理合集
1784 if (
1785 platformSetting.hasOwnProperty('mixInfo') &&
1786 typeof platformSetting.mixInfo === 'object' &&
1787 platformSetting.mixInfo.hasOwnProperty('mixId') &&
1788 platformSetting.mixInfo.hasOwnProperty('mixName')
1789 ) {
1790 // @ts-ignore
1791 publishParams.mix_id = platformSetting.mixInfo.mixId;
1792 }
1793
1794 // 处理POI
1795 if (
1796 platformSetting.hasOwnProperty('poiInfo') &&
1797 typeof platformSetting.poiInfo === 'object' &&
1798 platformSetting.poiInfo.hasOwnProperty('poiId') &&
1799 platformSetting.poiInfo.poiId !== ''
1800 ) {
1801 // @ts-ignore
1802 publishParams.poi_id = platformSetting.poiInfo.poiId;
1803 // @ts-ignore
1804 publishParams.poi_name = platformSetting.poiInfo.poiName;
1805 }
1806
1807 // 处理背景音乐
1808 if (
1809 platformSetting.hasOwnProperty('musicId') &&
1810 platformSetting.musicId !== ''
1811 ) {
1812 // @ts-ignore
1813 publishParams.music_id = platformSetting.musicId;
1814 }
1815
1816 // 处理定时
1817 if (
1818 platformSetting.hasOwnProperty('timingTime') &&
1819 platformSetting.timingTime &&
1820 platformSetting.timingTime > Date.now()
1821 ) {
1822 // @ts-ignore
1823 publishParams.timing = Math.floor(platformSetting.timingTime / 1000);
1824 }
1825
1826 return publishParams;
1827 }
1828
1829 /**
1830 * HEAD请求专用方法
1831 */
1832 private async makeHeadRequest(
1833 url: string,
1834 cookieString: string,
1835 ): Promise<any> {
1836 try {
1837 const response = await fetch(url, {
1838 method: 'HEAD',
1839 // @ts-ignore
1840 headers: {
1841 'User-Agent':
1842 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
1843 Accept: '*/*',
1844 'Accept-Language': 'zh-CN,zh;q=0.9',
1845 'Accept-Encoding': 'gzip, deflate, br, zstd',
1846 Connection: 'keep-alive',
1847 'Cache-Control': 'no-cache',
1848 Pragma: 'no-cache',
1849 Cookie: cookieString,
1850 Authority: 'creator.douyin.com',
1851 Priority: 'u=1, i',
1852 Referer: 'https://creator.douyin.com/creator-micro/home',
1853 'Sec-Ch-Ua':
1854 '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
1855 'Sec-Ch-Ua-Mobile': '?0',
1856 'Sec-Ch-Ua-Platform': '"Windows"',
1857 'Sec-Fetch-Dest': 'empty',
1858 'Sec-Fetch-Mode': 'cors',
1859 'Sec-Fetch-Site': 'same-origin',
1860 'X-Secsdk-Csrf-Request': 1,
1861 'X-Secsdk-Csrf-Version': '1.2.22',
1862 },
1863 });
1864
1865 // 获取响应头
1866 const headers: { [key: string]: string } = {};
1867 response.headers.forEach((value, key) => {
1868 headers[key] = value;
1869 });
1870
1871 const fft = {
1872 status: response.status,
1873 statusText: response.statusText,
1874 headers: headers,
1875 ok: response.ok,
1876 };
1877 return fft;
1878 } catch (error) {
1879 console.error('HEAD请求失败:', error);
1880 throw new Error(
1881 `HEAD请求失败: ${error instanceof Error ? error.message : String(error)}`,
1882 );
1883 }
1884 }
1885
1886 /**
1887 * 获取SecsdkCsrfToken
1888 * @param cookieString cookie字符串
1889 */
1890 private async getSecsdkCsrfToken(cookieString: string): Promise<string> {
1891 return new Promise(async (resolve, reject) => {
1892 try {
1893 const csrfTokenRes = await this.makeHeadRequest(
1894 this.getCsrfTokenUrl,
1895 cookieString,
1896 );
1897 if (csrfTokenRes.status !== 200) {
1898 console.error(
1899 '获取CSRF_TOKEN失败,状态码不是200:',
1900 csrfTokenRes.status,
1901 );
1902 reject('获取CSRF_TOKEN失败,请稍后重试!');
1903 return;
1904 }
1905
1906 const headers = csrfTokenRes.headers;
1907
1908 if (!headers['x-ware-csrf-token']) {
1909 console.error('获取CSRF_TOKEN失败,响应头中没有x-ware-csrf-token');
1910 reject('获取CSRF_TOKEN失败,请稍后重试!');
1911 return;
1912 }
1913
1914 const csrfToken = headers['x-ware-csrf-token'].split(',');
1915 resolve(csrfToken[1]);
1916 } catch (err) {
1917 console.error('获取CSRF Token过程出错:', err);
1918 reject(err);
1919 }
1920 });
1921 }
1922
1923 /**
1924 * 获取bd-Ticket请求头
1925 * @param tokens 用户令牌
1926 */
1927 private async getBdTicketHeaders(tokens: any): Promise<any> {
1928 return new Promise(async (resolve, reject) => {
1929 if (tokens === null || tokens === '' || tokens === undefined) {
1930 resolve({});
1931 return true;
1932 }
1933
1934 console.log('----------------------');
1935 try {
1936 tokens = JSON.parse(tokens);
1937 console.log(tokens);
1938 if (
1939 !tokens.hasOwnProperty('privateKey') ||
1940 !tokens.hasOwnProperty('webProtect')
1941 ) {
1942 reject('解析用户私钥失败,请重新授权!');
1943 return false;
1944 }
1945
1946 const { privateKey, webProtect } = tokens;
1947
1948 console.log(webProtect);
1949 // 获取bd请求参数
1950 const bdRes = await this.makeRequest(
1951 'http://116.62.154.231:7879/index/index/douyin',
1952 {
1953 method: 'POST',
1954 headers: {
1955 'Content-Type': 'application/json;charset=UTF-8',
1956 },
1957 data: {
1958 privateKey: privateKey,
1959 webProtect: webProtect,
1960 path: '/web/api/media/aweme/create/',
1961 },
1962 dataType: 'json',
1963 timeout: 15000,
1964 },
1965 '',
1966 );
1967 console.log('bdRes:', bdRes);
1968
1969 resolve(bdRes);
1970 } catch (err) {
1971 console.error('获取bd-Ticket失败:', err);
1972 reject(err);
1973 }
1974 });
1975 }
1976
1977 /**
1978 * 发布专用请求方法
1979 */
1980 private async makePublishRequest(
1981 url: string,
1982 options: any,
1983 proxy: string,
1984 ): Promise<any> {
1985 try {
1986 // 创建 FormData 对象
1987 const formData = new CustomFormData();
1988 const postData = options.data;
1989
1990 // 将数据添加到 FormData
1991 Object.keys(postData).forEach((key) => {
1992 if (postData[key] !== null && postData[key] !== undefined) {
1993 if (typeof postData[key] === 'object') {
1994 formData.append(key, JSON.stringify(postData[key]));
1995 } else {
1996 formData.append(key, postData[key]);
1997 }
1998 }
1999 });
2000
2001 const response = await requestNet({
2002 url,
2003 method: options.method,
2004 headers: {
2005 ...options.headers,
2006 },
2007 formData: formData,
2008 proxy,
2009 });
2010
2011 // 检查响应数据是否为空
2012 if (!response.data) {
2013 console.error(`响应数据为空`);
2014 throw new Error('服务器返回空数据');
2015 }
2016 return response.data;
2017 } catch (error) {
2018 console.error(`请求发生错误:`, error);
2019 throw new Error(
2020 `请求失败: ${error instanceof Error ? error.message : String(error)}`,
2021 );
2022 }
2023 }
2024
2025 // 获取话题
2026 async getTopics({ keyword }: { keyword: string }) {
2027 return await requestNet<DouyinTopicsSugResponse>({
2028 url: `${this.loginUrl}aweme/v1/search/challengesug/?aid=1&keyword=${keyword}`,
2029 method: 'GET',
2030 });
2031 }
2032
2033 // 获取位置数据
2034 async getLocation(params: {
2035 keywords: string;
2036 latitude: number;
2037 longitude: number;
2038 cookie: Electron.Cookie[];
2039 }) {
2040 return await requestNet<DouyinLocationDataResponse>({
2041 url: `${this.loginUrl}aweme/v1/life/video_api/search/poi/?${jsonToQueryString(
2042 {
2043 ...params,
2044 page: 1,
2045 from_webapp: 1,
2046 },
2047 )}`,
2048 headers: {
2049 cookie: CookieToString(params.cookie),
2050 },
2051 method: 'GET',
2052 });
2053 }
2054
2055 // 获取热点数据
2056 async getHotspotData(params: { query: string; cookie: Electron.Cookie[] }) {
2057 return await requestNet<DouyinHotDataResponse>({
2058 url: `${this.loginUrl}aweme/v1/hotspot/search/?${jsonToQueryString({
2059 ...params,
2060 count: 50,
2061 })}`,
2062 headers: {
2063 cookie: CookieToString(params.cookie),
2064 },
2065 method: 'GET',
2066 });
2067 }
2068
2069 // 获取默认热点数据,默认热点数据是没有搜索热点数据之前展示的默认数据
2070 async getAllHotspotData() {
2071 return await requestNet<DouyinAllHotDataResponse>({
2072 url: `${this.loginUrl}aweme/v1/hotspot/recommend`,
2073 method: 'GET',
2074 });
2075 }
2076
2077 // 查询抖音的活动列表
2078 async getActivity(cookie: Electron.Cookie[]) {
2079 return await requestNet<DouyinActivityListResponse>({
2080 url: `https://creator.douyin.com/web/api/media/activity/get/?page=1&size=9999`,
2081 headers: {
2082 cookie: CookieToString(cookie),
2083 },
2084 method: 'GET',
2085 });
2086 }
2087
2088 // 获取活动详情
2089 async getActivityDetails(cookie: Electron.Cookie[], activity_id: string) {
2090 return await requestNet<DouyinActivityDetailResponse>({
2091 url: `https://creator.douyin.com/web/api/media/activity/detail/?activity_id=${activity_id}`,
2092 headers: {
2093 cookie: CookieToString(cookie),
2094 },
2095 method: 'GET',
2096 });
2097 }
2098
2099 // 获取抖音活动标签
2100 async getActivityTags(cookie: Electron.Cookie[]) {
2101 return await requestNet<DouyinActivityTagsResponse>({
2102 url: `https://creator.douyin.com/web/api/media/activity/tags/query`,
2103 headers: {
2104 cookie: CookieToString(cookie),
2105 },
2106 method: 'GET',
2107 });
2108 }
2109
2110 // 获取合集
2111 async getMixList(cookie: Electron.Cookie[]) {
2112 return await requestNet<DouyinGetMixListResponse>({
2113 url: `https://creator.douyin.com/web/api/mix/list/?status=0%2C2&count=200&cursor=0`,
2114 headers: {
2115 cookie: CookieToString(cookie),
2116 },
2117 method: 'GET',
2118 });
2119 }
2120
2121 // 获取@的用户
2122 async getUsers(cookie: Electron.Cookie[], keyword: string, page: number) {
2123 return await requestNet<DouyinUserListResponse>({
2124 url: `https://creator.douyin.com/web/api/v2/discover/search/?search_source=publish_web&count=10&keyword=${keyword}&cursor=${(page - 1) * 10}&scene=1`,
2125 headers: {
2126 cookie: CookieToString(cookie),
2127 },
2128 method: 'GET',
2129 });
2130 }
2131
2132 /**
2133 * 查询抖音的作品列表
2134 * @param cookie
2135 * @param cursor
2136 * @returns
2137 */
2138 async getCreatorItems(cookie: Electron.Cookie[], cursor?: string) {
2139 const url = CommonUtils.buildUrl(
2140 'https://creator.douyin.com/aweme/v1/creator/item/list/',
2141 { cursor },
2142 );
2143 const res = await requestNet<DouyinCreatorListResponse>({
2144 url,
2145 headers: {
2146 cookie: CookieToString(cookie),
2147 },
2148 method: 'GET',
2149 });
2150 return res;
2151 }
2152
2153 // 获取搜索作品列表
2154 async getSearchNodeList(
2155 cookie: Electron.Cookie[],
2156 qe: string, // 搜索关键词
2157 pageInfo: {
2158 pcursor?: string;
2159 count?: number;
2160 postFirstId?: string;
2161 },
2162 ) {
2163 const pcursor =
2164 pageInfo.pcursor && Number(pageInfo.pcursor) < 16 ? 0 : pageInfo.pcursor;
2165 const count = Number(pcursor) > 10 ? 10 : 20;
2166
2167 const gets: any = {
2168 device_platform: 'webapp',
2169 aid: '6383',
2170 keyword: qe,
2171 offset: pcursor,
2172 count: count,
2173 };
2174 if (pageInfo.postFirstId) {
2175 gets.search_id = pageInfo.postFirstId;
2176 }
2177 const thisUri = `https://www.douyin.com/aweme/v1/web/search/item/?${jsonToQueryString(
2178 gets,
2179 )}`;
2180 console.log('thisUrithisUrithisUri', thisUri);
2181 // 方法
2182 const res = await requestNet<any>({
2183 url: thisUri,
2184 headers: {
2185 cookie: CookieToString(cookie),
2186 'User-Agent': this.defaultUserAgent,
2187 referer: encodeURI(
2188 'https://www.douyin.com/root/search/' + qe + '?type=video',
2189 ),
2190 },
2191 method: 'GET',
2192 });
2193 return res;
2194 }
2195
2196 // 查看作品的评论列表
2197 async getCreatorCommentList(
2198 cookie: Electron.Cookie[],
2199 item_id: string, // 作品ID
2200 pageInfo: {
2201 cursor?: string;
2202 count?: number;
2203 },
2204 ) {
2205 const res = await requestNet<DouyinCreatorCommentListResponse>({
2206 url: CommonUtils.buildUrl(
2207 `https://creator.douyin.com/aweme/v1/creator/comment/list/`,
2208 {
2209 cursor: pageInfo.cursor,
2210 count: pageInfo.count,
2211 item_id: item_id,
2212 sort: 'TIME',
2213 },
2214 ),
2215 headers: {
2216 cookie: CookieToString(cookie),
2217 },
2218 method: 'GET',
2219 });
2220 return res;
2221 }
2222
2223 // 查看作品的评论列表
2224 async getCreatorCommentListByOther(
2225 cookie: Electron.Cookie[],
2226 item_id: string, // 作品ID
2227 pageInfo: {
2228 cursor?: string;
2229 count?: number;
2230 },
2231 ) {
2232 // https://www-hj.douyin.com/aweme/v1/web/comment/list/?
2233 // // aweme_id=7483006686274374962&
2234 // // cursor=20&count=10&
2235 // // a_bogus=mjsjDq7jDpAcFdFb8KEfC5Fl6g6ArTSyNeidWSaTyPY4T1UTpbPUNPb9GxwoA1vPFRBhhH-73VM%2FbDdbO0UwZo9pwmkvuKiRz02C9zmoMHZ3TTv2XNWsCvSELiPTUCsYY%2FA9i2RRXs0KId5WnH9iAp17u%2FvrmRfdMH-XV2TjE9um0ASjhx%2FIa5JBxhwqjD%3D%3D
2236
2237 const thisUri = `https://www.douyin.com/aweme/v1/web/comment/list/?${jsonToQueryString(
2238 {
2239 cursor: pageInfo.cursor || 0,
2240 count: pageInfo.count || 20,
2241 aweme_id: item_id,
2242 a_bogus:
2243 'dX0fgqUEY2mfFdKGuOfg743UWS2/Nsuyz-idReZPHOOLT7lGmRPGpPSZbozcYEW5MWB0h937iVllYxdcKsXkZKrpwmhvS/7RsUI998so0qqpT0hDEqfNCwWT9JaT0cwL8CKbJARVUzmc2dA4D1r0UB-JH/Pn4mipQHaWdnUGT9tfgM49PrFxuOtDiXzx5OI41f==',
2244 },
2245 )}`;
2246 // ???
2247 const res = await requestNet<DouyinHotDataResponse>({
2248 url: thisUri,
2249 headers: {
2250 cookie: CookieToString(cookie),
2251 },
2252 method: 'GET',
2253 });
2254
2255 return res;
2256 }
2257
2258 // 查看评论的回复列表
2259 async getCreatorCommentReplyList(
2260 cookie: Electron.Cookie[],
2261 comment_id: string, // 评论ID
2262 pageInfo: {
2263 cursor?: string;
2264 count?: number;
2265 } = {
2266 cursor: '0',
2267 count: 10,
2268 },
2269 ) {
2270 const res = await requestNet<DouyinCreatorCommentListResponse>({
2271 url: CommonUtils.buildUrl(
2272 `https://creator.douyin.com/aweme/v1/creator/comment/reply/list/`, // msToken=xxx
2273 {
2274 comment_id: comment_id,
2275 cursor: pageInfo.cursor,
2276 count: pageInfo.count,
2277 },
2278 ),
2279 headers: {
2280 cookie: CookieToString(cookie),
2281 },
2282 method: 'GET',
2283 });
2284
2285 return res;
2286 }
2287
2288 /**
2289 * 发送表单数据请求
2290 * @param url 请求URL
2291 * @param options 请求选项
2292 * @param retryOptions 重试选项
2293 */
2294 private async postFormData(
2295 url: string,
2296 options: any,
2297 retryOptions: { maxRetries?: number; retryDelay?: number } = {},
2298 ): Promise<any> {
2299 const maxRetries = retryOptions.maxRetries || 3;
2300 const retryDelay = retryOptions.retryDelay || 2000;
2301 let retryCount = 0;
2302
2303 while (retryCount < maxRetries) {
2304 try {
2305 // 创建 FormData 对象
2306 const formData = new FormData();
2307 const postData = options.data;
2308
2309 // 将数据添加到 FormData
2310 Object.keys(postData).forEach((key) => {
2311 if (typeof postData[key] === 'object') {
2312 formData.append(key, JSON.stringify(postData[key]));
2313 } else {
2314 formData.append(key, postData[key]);
2315 }
2316 });
2317
2318 const response = await fetch(url, {
2319 method: options.method || 'POST',
2320 headers: {
2321 ...options.headers,
2322 },
2323 body: formData,
2324 });
2325
2326 // 检查HTTP状态码
2327 if (!response.ok) {
2328 console.error(`HTTP错误状态码: ${response.status}`);
2329 if (retryCount < maxRetries - 1) {
2330 retryCount++;
2331 await new Promise((resolve) => setTimeout(resolve, retryDelay));
2332 continue;
2333 }
2334 throw new Error(`服务器返回错误状态码: ${response.status}`);
2335 }
2336
2337 const responseText = await response.text();
2338
2339 // 检查响应数据是否为空
2340 if (!responseText || responseText.trim() === '') {
2341 console.error(
2342 `响应数据为空,尝试次数: ${retryCount + 1}/${maxRetries}`,
2343 );
2344
2345 if (retryCount < maxRetries - 1) {
2346 retryCount++;
2347 console.log(`等待${retryDelay / 1000}秒后重试...`);
2348 await new Promise((resolve) => setTimeout(resolve, retryDelay));
2349 continue;
2350 }
2351
2352 throw new Error(
2353 '服务器多次返回空数据,请检查网络连接或抖音服务器状态',
2354 );
2355 }
2356
2357 try {
2358 const result = JSON.parse(responseText);
2359
2360 if (!result) {
2361 console.error(`解析后的数据为空`);
2362 throw new Error('解析后的数据为空');
2363 }
2364
2365 return result;
2366 } catch (err) {
2367 console.error(`解析响应数据失败:`, err);
2368 console.error(`导致错误的原始数据:`, responseText);
2369 throw new Error(
2370 `解析响应数据失败: ${err instanceof Error ? err.message : String(err)}`,
2371 );
2372 }
2373 } catch (error) {
2374 if (retryCount < maxRetries - 1) {
2375 retryCount++;
2376 console.log(
2377 `请求失败,${retryDelay / 1000}秒后进行第${retryCount}次重试...`,
2378 );
2379 await new Promise((resolve) => setTimeout(resolve, retryDelay));
2380 continue;
2381 }
2382
2383 console.error(`请求发生错误:`, error);
2384 throw new Error(
2385 `请求失败: ${error instanceof Error ? error.message : String(error)}`,
2386 );
2387 }
2388 }
2389
2390 // 这里不应该被执行到,但为了类型安全添加
2391 throw new Error('请求失败: 超过最大重试次数');
2392 }
2393
2394 // 点赞
2395 async creatorDianzanOther(
2396 cookie: Electron.Cookie[],
2397 data: any,
2398 ): Promise<{
2399 extra: {
2400 fatal_item_ids: string[];
2401 logid: string; // '2025040322304072588F91E9D7AE3AA42B';
2402 now: number; // 1743690640000;
2403 };
2404 log_pb: {
2405 impr_id: string; // '2025040322304072588F91E9D7AE3AA42B'
2406 };
2407 status_code: number; // 0;
2408 is_digg: number; // 0;
2409 }> {
2410 const thisUri = `https://www.douyin.com/aweme/v1/web/commit/item/digg/?${jsonToQueryString(
2411 {
2412 aid: '6383',
2413 },
2414 )}`;
2415 const cookieString = CommonUtils.convertCookieToJson(cookie);
2416 const csrfToken = await this.getSecsdkCsrfToken(cookieString);
2417
2418 // 使用新的 postRequest 方法替代 makePublishRequest
2419 const res = await this.postFormData(
2420 thisUri,
2421 {
2422 method: 'POST',
2423 headers: {
2424 Cookie: cookieString,
2425 'X-Secsdk-Csrf-Token': csrfToken,
2426 referer: `https://www.douyin.com/video/${data.aweme_id}`,
2427 'user-agent': this.defaultUserAgent,
2428 },
2429 data: data,
2430 },
2431 { maxRetries: 0, retryDelay: 2000 },
2432 );
2433
2434 return res;
2435 }
2436
2437 // 收藏
2438 async creatorShoucangOther(cookie: Electron.Cookie[], data: any) {
2439 const thisUri = `https://www.douyin.com/aweme/v1/web/aweme/collect/?${jsonToQueryString(
2440 {
2441 aid: '6383',
2442 },
2443 )}`;
2444 const cookieString = CommonUtils.convertCookieToJson(cookie);
2445 const csrfToken = await this.getSecsdkCsrfToken(cookieString);
2446
2447 // 使用新的 postRequest 方法替代 makePublishRequest
2448 const res = await this.postFormData(
2449 thisUri,
2450 {
2451 method: 'POST',
2452 headers: {
2453 Cookie: cookieString,
2454 'X-Secsdk-Csrf-Token': csrfToken,
2455 referer: `https://www.douyin.com/video/${data.aweme_id}`,
2456 },
2457 data: data,
2458 },
2459 { maxRetries: 1, retryDelay: 2000 },
2460 );
2461
2462 return res;
2463 }
2464
2465 // 回复其他人的评论
2466 async creatorCommentReplyOther(cookie: Electron.Cookie[], data: any) {
2467 console.log('wentipaicha1');
2468 const thisUri = `https://www.douyin.com/aweme/v1/web/comment/publish/?${jsonToQueryString(
2469 {
2470 aid: '6383',
2471 },
2472 )}`;
2473 console.log('wentipaicha2');
2474 const cookieString = CommonUtils.convertCookieToJson(cookie);
2475 console.log('wentipaicha3');
2476 const csrfToken = await this.getSecsdkCsrfToken(cookieString);
2477 console.log('wentipaicha4');
2478 const res = await this.postFormData(thisUri, {
2479 method: 'POST',
2480 headers: {
2481 Cookie: cookieString,
2482 'X-Secsdk-Csrf-Token': csrfToken,
2483 referer: `https://www.douyin.com/video/${data.aweme_id}`,
2484 },
2485 data: data,
2486 });
2487 console.log('douyin index ------ res', res);
2488 return res;
2489 }
2490
2491 // 作品的评论回复
2492 async creatorCommentReply(
2493 cookie: Electron.Cookie[],
2494 data: {
2495 comment_Id: string; // 空字符串, 直接回复
2496 item_id: string; // '@j/do779EQE//uctS8rzvvch6oCaTZCH0JqwsPqxpgahhkia+W5A7RJEoPQpq6PZl7wq9uxSqSWCjcIdbPzF8fQ==';
2497 text: string; //'哈哈哈';
2498 },
2499 ) {
2500 const cookieString = CommonUtils.convertCookieToJson(cookie);
2501 const csrfToken = await this.getSecsdkCsrfToken(cookieString);
2502 const res = await requestNet<DouyinNewCommentResponse>({
2503 url: `https://creator.douyin.com/aweme/v1/creator/comment/reply/`,
2504 headers: {
2505 cookie: cookieString,
2506 'X-Secsdk-Csrf-Token': csrfToken,
2507 },
2508 method: 'POST',
2509 body: data,
2510 });
2511 return res;
2512 }
2513
2514 /**
2515 * 发送POST请求
2516 * @param url 请求URL
2517 * @param options 请求选项
2518 * @param retryOptions 重试选项
2519 */
2520 private async postRequest(
2521 url: string,
2522 options: any,
2523 retryOptions: { maxRetries?: number; retryDelay?: number } = {},
2524 ): Promise<any> {
2525 const maxRetries = retryOptions.maxRetries || 3;
2526 const retryDelay = retryOptions.retryDelay || 2000;
2527 let retryCount = 0;
2528
2529 while (retryCount < maxRetries) {
2530 try {
2531 const response = await fetch(url, {
2532 method: options.method || 'POST',
2533 headers: {
2534 'Content-Type': 'application/json',
2535 ...options.headers,
2536 },
2537 body: JSON.stringify(options.data),
2538 });
2539
2540 // 检查HTTP状态码
2541 if (!response.ok) {
2542 console.error(`HTTP错误状态码: ${response.status}`);
2543 if (retryCount < maxRetries - 1) {
2544 retryCount++;
2545 await new Promise((resolve) => setTimeout(resolve, retryDelay));
2546 continue;
2547 }
2548 throw new Error(`服务器返回错误状态码: ${response.status}`);
2549 }
2550
2551 const responseText = await response.text();
2552
2553 // 检查响应数据是否为空
2554 if (!responseText || responseText.trim() === '') {
2555 console.error(
2556 `响应数据为空,尝试次数: ${retryCount + 1}/${maxRetries}`,
2557 );
2558
2559 if (retryCount < maxRetries - 1) {
2560 retryCount++;
2561 console.log(`等待${retryDelay / 1000}秒后重试...`);
2562 await new Promise((resolve) => setTimeout(resolve, retryDelay));
2563 continue;
2564 }
2565
2566 throw new Error(
2567 '服务器多次返回空数据,请检查网络连接或抖音服务器状态',
2568 );
2569 }
2570
2571 try {
2572 const result = JSON.parse(responseText);
2573
2574 if (!result) {
2575 console.error(`解析后的数据为空`);
2576 throw new Error('解析后的数据为空');
2577 }
2578
2579 return result;
2580 } catch (err) {
2581 console.error(`解析响应数据失败:`, err);
2582 console.error(`导致错误的原始数据:`, responseText);
2583 throw new Error(
2584 `解析响应数据失败: ${err instanceof Error ? err.message : String(err)}`,
2585 );
2586 }
2587 } catch (error) {
2588 if (retryCount < maxRetries - 1) {
2589 retryCount++;
2590 console.log(
2591 `请求失败,${retryDelay / 1000}秒后进行第${retryCount}次重试...`,
2592 );
2593 await new Promise((resolve) => setTimeout(resolve, retryDelay));
2594 continue;
2595 }
2596
2597 console.error(`请求发生错误:`, error);
2598 throw new Error(
2599 `请求失败: ${error instanceof Error ? error.message : String(error)}`,
2600 );
2601 }
2602 }
2603
2604 // 这里不应该被执行到,但为了类型安全添加
2605 throw new Error('请求失败: 超过最大重试次数');
2606 }
2607 }
2608
2609 // 导出服务实例
2610 export const douyinService = new DouyinService();
2611
2611 lines TYPESCRIPT