返回 AiToEarn
index.ts
1 import { screen, BrowserWindow, session } from 'electron';
2 import { CommonUtils } from '../../util/common';
3 import path from 'path';
4 import { FileUtils } from '../../util/file';
5 import { CookieToString, getFileContent } from '../utils';
6 import requestNet from '../requestNet';
7 import {
8 CommentInfo,
9 SphGetCommentListResponse,
10 SphGetPostListResponse,
11 WeChatLocationData,
12 WeChatVideoApiResponse,
13 WeChatVideoUserData,
14 WxSPHGetMixListResponse,
15 } from './wxShp.type';
16 import { v4 as uuidv4 } from 'uuid';
17 import { RetryWhile } from '../../../commont/utils';
18 interface UserInfo {
19 authorId: string;
20 nickname: string;
21 avatar: string;
22 fansCount: number;
23 }
24
25 interface UploadArguments {
26 apptype: any;
27 filetype: any;
28 weixinnum: any;
29 filekey: string;
30 filesize: any;
31 taskid: string;
32 scene: any;
33 [key: string]: any;
34 }
35
36 export class ShipinhaoService {
37 private defaultUserAgent =
38 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
39 private loginUrl = 'https://channels.weixin.qq.com';
40 private getUserInfoUrl =
41 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/auth/auth_data';
42 private getDashboardUrl =
43 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/statistic/new_post_total_data';
44 private getMixListUrl =
45 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/collection/get_collection_list?';
46 private getSearchUserListUrl =
47 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/helper/helper_search_finder_account';
48 private getPositionListUrl =
49 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/helper/helper_search_location';
50 private getPublishTraceKeyUrl =
51 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/get-finder-post-trace-key';
52 private getPublishUploadParamsUrl =
53 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/helper/helper_upload_params';
54 private getApplyUploadDfsUrl =
55 'https://finderassistancea.video.qq.com/applyuploaddfs';
56 private uploadpartdfsUrl =
57 'https://finderassistancea.video.qq.com/uploadpartdfs';
58 private uploadCompleteUrl =
59 'https://finderassistancea.video.qq.com/completepartuploaddfs';
60 private postClipVideoUrl =
61 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/post_clip_video';
62 private postClipVideoResultUrl =
63 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/post_clip_video_result';
64 private postCreateVideoUrl =
65 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/post_create';
66 private getUserWorkListUrl =
67 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/post_list';
68 private checkFinderUrl =
69 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/check_finder_comm_face';
70 private fileBlockSize = 8388608;
71 private windowName = 'shipinhao';
72 private cookieCheckField = 'sessionid';
73 private cookieIntervalList: { [key: string]: NodeJS.Timeout } = {};
74 private windowMap: { [key: number]: BrowserWindow } = {}; // 添加窗口引用存储
75 private callback?: (progress: number, msg?: string) => void;
76
77 /**
78 * 获取网站登录cookie
79 */
80 private async filterCookie(
81 winContentsId: number,
82 partition: string,
83 ): Promise<Electron.Cookie[]> {
84 return new Promise((resolve, reject) => {
85 this.windowMap[winContentsId].webContents.on(
86 'did-navigate',
87 async (event, url) => {
88 try {
89 const cookies = await this.windowMap[
90 winContentsId
91 ].webContents.session.cookies.get({});
92 const alreadyLogin = cookies.some(
93 (item) =>
94 item.name.includes(this.cookieCheckField) && item.value !== '',
95 );
96 // 如果获取到登录状态cookie
97 if (alreadyLogin) {
98 // 关闭定时器
99 if (this.cookieIntervalList.hasOwnProperty(winContentsId)) {
100 clearInterval(this.cookieIntervalList[winContentsId]);
101 delete this.cookieIntervalList[winContentsId];
102 }
103 // 返回Cookie信息
104 resolve(cookies);
105 }
106 } catch (error) {
107 console.error(error);
108 reject('获取网站cookie失败');
109 }
110 },
111 );
112 });
113 }
114
115 /**
116 * 获取用户信息
117 */
118 async getUserInfo(cookies: Electron.Cookie[]): Promise<UserInfo> {
119 return new Promise(async (resolve, reject) => {
120 const cookieString = CommonUtils.convertCookieToJson(cookies);
121 try {
122 const res = await this.makeRequest(
123 this.getUserInfoUrl,
124 {
125 method: 'POST',
126 headers: {
127 Origin: 'https://channels.weixin.qq.com',
128 Referer: 'https://channels.weixin.qq.com/platform',
129 Cookie: cookieString,
130 },
131 timeout: 15000,
132 },
133 '',
134 );
135 if (res.errCode === 0) {
136 resolve({
137 authorId: res.data.finderUser.uniqId ?? '',
138 nickname: res.data.finderUser.nickname ?? '',
139 avatar: res.data.finderUser.headImgUrl ?? '',
140 fansCount: res.data.finderUser.fansCount ?? 0,
141 });
142 } else {
143 reject(res.data?.errMsg ?? '未知错误');
144 }
145 } catch (err) {
146 reject(err);
147 }
148 });
149 }
150
151 /**
152 * 获取账户信息
153 * @param cookies Cookie信息
154 * @param startDate 可选的开始日期,格式:YYYY-MM-DD
155 * @param endDate 可选的结束日期,格式:YYYY-MM-DD
156 */
157 async getDashboardFunc(
158 cookies: Electron.Cookie[],
159 startDate?: string,
160 endDate?: string,
161 ): Promise<{
162 success: boolean;
163 data: Array<{
164 date?: string;
165 zhangfen: number;
166 bofang: number;
167 pinglun: number;
168 dianzan: number;
169 fenxiang: number;
170 zhuye: number;
171 }>;
172 res: any;
173 }> {
174 return new Promise(async (resolve, reject) => {
175 const cookieString = CommonUtils.convertCookieToJson(cookies);
176
177 // 如果没有传入日期,使用默认的前天到昨天
178 const endTs = endDate
179 ? Math.floor(new Date(endDate).setHours(0, 0, 0, 0) / 1000).toString()
180 : Math.floor(
181 new Date(new Date().setDate(new Date().getDate() - 1)).setHours(
182 0,
183 0,
184 0,
185 0,
186 ) / 1000,
187 ).toString();
188
189 const startTs = startDate
190 ? Math.floor(new Date(startDate).setHours(0, 0, 0, 0) / 1000).toString()
191 : Math.floor(
192 new Date(new Date().setDate(new Date().getDate() - 2)).setHours(
193 0,
194 0,
195 0,
196 0,
197 ) / 1000,
198 ).toString();
199
200 try {
201 const res = await this.makeRequest(
202 this.getDashboardUrl,
203 {
204 method: 'POST',
205 headers: {
206 'Content-Type': 'application/json',
207 Origin: 'https://channels.weixin.qq.com',
208 Referer: 'https://channels.weixin.qq.com/platform',
209 Cookie: cookieString,
210 },
211 timeout: 15000,
212 data: {
213 endTs,
214 interval: 3,
215 startTs,
216 timestamp: new Date().getTime(),
217 },
218 },
219 '',
220 );
221
222 if (res.errCode === 0) {
223 if (endDate && startDate) {
224 // 如果传入了日期范围,返回数组格式
225 const dataArray = [];
226 const dates = res.data.totalData.browse;
227 const followData = res.data.totalData.follow;
228 const browseData = res.data.totalData.browse;
229 const commentData = res.data.totalData.comment;
230 const likeData = res.data.totalData.like;
231 const forwardData = res.data.totalData.forward;
232 const snscoverData = res.data.totalData.snscover;
233
234 for (let i = 0; i < dates.length; i++) {
235 dataArray.push({
236 date: dates[i],
237 zhangfen: followData[i] || 0,
238 bofang: browseData[i] || 0,
239 pinglun: commentData[i] || 0,
240 dianzan: likeData[i] || 0,
241 fenxiang: forwardData[i] || 0,
242 zhuye: snscoverData[i] || 0,
243 });
244 }
245
246 resolve({
247 success: true,
248 data: dataArray,
249 res,
250 });
251 } else {
252 // 如果传入了日期范围,返回数组格式
253 const dataArray = [];
254 const indexs = res.data.totalData.browse.length - 1;
255 const followData = res.data.totalData.follow;
256 const browseData = res.data.totalData.browse;
257 const commentData = res.data.totalData.comment;
258 const likeData = res.data.totalData.like;
259 const forwardData = res.data.totalData.forward;
260 const snscoverData = res.data.totalData.snscover;
261
262 dataArray.push({
263 zhangfen: followData[indexs] || 0,
264 bofang: browseData[indexs] || 0,
265 pinglun: commentData[indexs] || 0,
266 dianzan: likeData[indexs] || 0,
267 fenxiang: forwardData[indexs] || 0,
268 zhuye: snscoverData[indexs] || 0,
269 });
270
271 resolve({
272 success: true,
273 data: dataArray,
274 res,
275 });
276 }
277 } else {
278 reject(res.data?.errMsg ?? '未知错误');
279 }
280 } catch (err) {
281 reject(err);
282 }
283 });
284 }
285
286 /**
287 * 授权|预览
288 */
289 async loginOrView(
290 authModel: 'login' | 'view',
291 cookies?: any,
292 ): Promise<{
293 success: boolean;
294 data?: { cookie: any; userInfo: any };
295 error?: string;
296 }> {
297 return new Promise(async (resolve, reject) => {
298 try {
299 // 如果是查看,则判断cookie是否过期
300 const winRes = await this.createAuthorizationWindow(
301 authModel === 'view' ? cookies : null,
302 );
303 const { winContentsId, partition } = winRes;
304 // 获取登录态Cookie
305 const newCookies = await this.filterCookie(winContentsId, partition);
306
307 // 获取登录态用户信息
308 const userInfo = await this.getUserInfo(newCookies);
309 // 如果是授权,则需要关闭授权窗体
310 if (authModel === 'login') {
311 const win = this.windowMap[winContentsId];
312 if (win && !win.isDestroyed()) {
313 // 移除所有事件监听器
314 win.webContents.removeAllListeners();
315 // 关闭窗口
316 win.destroy();
317 // 清理引用
318 delete this.windowMap[winContentsId];
319 } else {
320 }
321 }
322
323 // 返回cookie
324 resolve({
325 success: true,
326 data: {
327 cookie: newCookies,
328 userInfo: userInfo,
329 },
330 });
331 } catch (e) {
332 resolve({
333 success: false,
334 error: '获取失败',
335 });
336 }
337 });
338 }
339
340 /**
341 * 创建授权窗口
342 */
343 private async createAuthorizationWindow(cookies: any = null) {
344 return new Promise<{ winContentsId: number; partition: string }>(
345 async (resolve, reject) => {
346 try {
347 // 生成随机partition
348 const partition = Date.now().toString();
349
350 // 获取屏幕尺寸
351 const { width, height } = screen.getPrimaryDisplay().workAreaSize;
352
353 // 创建窗口
354 const win = new BrowserWindow({
355 width: Math.ceil(width * 0.8),
356 height: Math.ceil(height * 0.8),
357 show: false,
358 icon: path.join(process.cwd(), 'public', 'images', 'logo-32.png'),
359 webPreferences: {
360 contextIsolation: false,
361 nodeIntegration: false,
362 partition: partition,
363 },
364 });
365 win.show();
366
367 const winContentsId = win.webContents.id;
368 // 存储窗口引用
369 this.windowMap[winContentsId] = win;
370
371 // 如果是查看并且有cookie传入,如果有,判断登录态,如果登录态还在,则设置cookie
372 if (cookies) {
373 try {
374 const loginStatus = await this.checkLoginStatus(cookies);
375 if (loginStatus) {
376 const cookiesObj =
377 typeof cookies === 'string' ? JSON.parse(cookies) : cookies;
378 for (const cookie of cookiesObj) {
379 await session.fromPartition(partition).cookies.set({
380 url: this.loginUrl,
381 name: cookie.name,
382 value: cookie.value,
383 domain: cookie.domain,
384 path: cookie.path,
385 });
386 }
387 }
388 } catch (err) {
389 console.error('Set cookies error:', err);
390 }
391 }
392
393 // 设置用户代理
394 win.webContents.setUserAgent(this.defaultUserAgent);
395
396 // 监听导航错误
397 win.webContents.on(
398 'did-fail-load',
399 (event, errorCode, errorDescription) => {
400 console.error('Page failed to load:', errorDescription);
401 // 不要立即 reject,给一些时间重试
402 if (errorCode === -3) {
403 // ERR_ABORTED 通常是因为重定向,不需要处理
404 return;
405 }
406 reject(new Error(`Failed to load page: ${errorDescription}`));
407 },
408 );
409
410 // 显示页面并设置置顶
411 win.once('ready-to-show', () => {
412 win.focus();
413 win.center();
414 win.setAlwaysOnTop(true);
415 win.setAlwaysOnTop(false);
416 });
417
418 // 监听窗口销毁
419 win.webContents.on('destroyed', () => {
420 if (this.cookieIntervalList.hasOwnProperty(winContentsId)) {
421 clearInterval(this.cookieIntervalList[winContentsId]);
422 delete this.cookieIntervalList[winContentsId];
423 }
424 // 清理窗口引用
425 delete this.windowMap[winContentsId];
426 });
427
428 let isResolved = false;
429
430 // 监听窗口加载完成
431 win.webContents.on('did-finish-load', () => {
432 if (!isResolved) {
433 isResolved = true;
434 resolve({ winContentsId, partition });
435 }
436 });
437
438 // 加载登录页
439 try {
440 win.loadURL(this.loginUrl, {
441 userAgent: this.defaultUserAgent,
442 httpReferrer: 'https://channels.weixin.qq.com',
443 });
444
445 // 只有在页面没有触发 did-finish-load 的情况下才 resolve
446 setTimeout(() => {
447 if (!isResolved) {
448 isResolved = true;
449 resolve({ winContentsId, partition });
450 }
451 }, 5000); // 5秒后如果还没有 resolve,就强制 resolve
452 } catch (err: any) {
453 console.error('Load URL error:', err);
454 // 如果是重定向错误,不需要 reject
455 if (err.code !== -3 && !isResolved) {
456 reject(err);
457 }
458 }
459 } catch (err: any) {
460 console.error('Create window error:', err);
461 reject(err);
462 }
463 },
464 );
465 }
466
467 /**
468 * 检查用户登录是否过期
469 */
470 async checkLoginStatus(cookies: string): Promise<boolean> {
471 const cookieString = CommonUtils.convertCookieToJson(cookies);
472 try {
473 const res = await this.makeRequest(
474 this.getUserInfoUrl,
475 {
476 method: 'POST',
477 headers: {
478 Origin: 'https://channels.weixin.qq.com',
479 Referer: 'https://channels.weixin.qq.com/platform',
480 Cookie: cookieString,
481 },
482 timeout: 15000,
483 },
484 '',
485 );
486 return res.errCode === 0;
487 } catch (err) {
488 console.log('-------- checkLoginStatus error ---', err);
489 return false;
490 }
491 }
492
493 /**
494 * 通用请求方法
495 */
496 private async makeRequest(
497 url: string,
498 options: any,
499 proxy: string,
500 ): Promise<any> {
501 return new Promise(async (resolve, reject) => {
502 try {
503 const res = await requestNet({
504 url: url,
505 method: options.method,
506 headers: options.headers,
507 body: options.data,
508 proxy,
509 });
510 resolve(res.data);
511 } catch (e) {
512 reject(e);
513 }
514 });
515 }
516
517 /**
518 * 上传文件到远程服务器
519 * @param url 上传地址
520 * @param fileContent 文件内容
521 * @param headers 请求头
522 * @param method HTTP 方法
523 * @param proxy
524 */
525 private async uploadFile(
526 url: string,
527 fileContent: Buffer,
528 headers: any,
529 method: string = 'PUT',
530 proxy: string,
531 ): Promise<any> {
532 return new Promise(async (resolve, reject) => {
533 try {
534 const res = await requestNet({
535 url: url,
536 method: method as 'POST',
537 isFile: true,
538 headers: headers,
539 body: fileContent,
540 proxy,
541 });
542 resolve(res.data);
543 } catch (e) {
544 console.error('上传文件失败:', e);
545 reject(e);
546 }
547 });
548 }
549
550 /**
551 * 获取唯一任务uuid
552 */
553 private getUniqueTaskId(): string {
554 return 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace(
555 /[xy]/g,
556 function (c) {
557 const r = (Math.random() * 16) | 0,
558 v = c === 'x' ? r : (r & 0x3) | 0x8;
559 return v.toString(16);
560 },
561 );
562 }
563
564 /**
565 * 获取发布TraceKey
566 * @param cookieString
567 * @param proxy
568 */
569 private async getPublishTraceKey(
570 cookieString: string,
571 proxy: string,
572 ): Promise<string> {
573 const requestParams = {
574 objectId: '',
575 pluginSessionId: null,
576 rawKeyBuff: null,
577 reqScene: 7,
578 scene: 7,
579 };
580
581 const res = await this.makeRequest(
582 this.getPublishTraceKeyUrl,
583 {
584 method: 'POST',
585 headers: {
586 Cookie: cookieString,
587 },
588 body: requestParams,
589 },
590 proxy,
591 );
592
593 if (res.errCode === 0) {
594 return res.data.traceKey;
595 } else {
596 throw new Error('获取TraceKey失败,失败原因:' + (res.errMsg ?? '未知'));
597 }
598 }
599
600 /**
601 * 获取发布上传参数
602 * @param cookieString
603 * @param proxy
604 */
605 private async getPublishUploadParams(
606 cookieString: string,
607 proxy: string,
608 ): Promise<any> {
609 const requestParams = {
610 reqScene: 7,
611 scene: 7,
612 };
613
614 const res = await this.makeRequest(
615 this.getPublishUploadParamsUrl,
616 {
617 method: 'POST',
618 headers: {
619 Cookie: cookieString,
620 },
621 body: requestParams,
622 },
623 proxy,
624 );
625
626 if (res.errCode === 0) {
627 return res.data;
628 } else {
629 throw new Error('获取上传参数失败,失败原因:' + (res.errMsg ?? '未知'));
630 }
631 }
632
633 /**
634 * 上传视频文件
635 * @param filePath
636 * @param uploadParams
637 * @param filePartInfo
638 * @param proxy
639 */
640 private async uploadVideoFile(
641 filePath: string,
642 uploadParams: any,
643 filePartInfo: any,
644 proxy: string,
645 ): Promise<string> {
646 try {
647 // 拼接请求参数
648 const uploadArguments: UploadArguments = {
649 apptype: uploadParams.appType,
650 filetype: uploadParams.videoFileType,
651 weixinnum: uploadParams.uin,
652 filekey: filePath.split(path.sep).slice(-1)[0],
653 filesize: filePartInfo.fileSize,
654 taskid: this.getUniqueTaskId(),
655 scene: uploadParams.scene,
656 };
657
658 let uploadArgumentsString = '';
659 for (const key in uploadArguments) {
660 uploadArgumentsString += `${key}=${uploadArguments[key]}&`;
661 }
662
663 // 获取上传id - 使用 uploadFile 方法
664 const requestBody = {
665 BlockPartLength: filePartInfo.blockInfo,
666 BlockSum: filePartInfo.blockInfo.length,
667 };
668
669 const uploadIdRes = await this.uploadFile(
670 this.getApplyUploadDfsUrl,
671 Buffer.from(JSON.stringify(requestBody)),
672 {
673 Authorization: uploadParams.authKey,
674 'Content-Type': 'application/json',
675 'X-Arguments': uploadArgumentsString,
676 },
677 undefined,
678 proxy,
679 );
680
681 if (!uploadIdRes.UploadID) {
682 throw new Error('上传视频失败,失败原因:获取上传id失败');
683 }
684
685 const uploadId = uploadIdRes.UploadID;
686 const uploadPartInfo: any[] = [];
687
688 // 分片上传文件
689 for (let i = 0; i < filePartInfo.blockInfo.length; i++) {
690 let errorMsg = '';
691 const isSuccess = await RetryWhile(async () => {
692 if (this.callback)
693 this.callback(
694 50,
695 `上传视频(${i}/${filePartInfo.blockInfo.length})`,
696 );
697 console.log(
698 `开始上传第 ${i + 1}/${filePartInfo.blockInfo.length} 个分片`,
699 );
700 const chunkStart = i === 0 ? 0 : filePartInfo.blockInfo[i - 1];
701 const chunkEnd = filePartInfo.blockInfo[i] - 1;
702 const chunkContent = await FileUtils.getFilePartContent(
703 filePath,
704 chunkStart,
705 chunkEnd,
706 );
707
708 // 开始上传
709 const uploadUrl = `${this.uploadpartdfsUrl}?UploadID=${uploadId}&PartNumber=${i + 1}&QuickUpload=2`;
710 const uploadPartRes = await this.uploadFile(
711 uploadUrl,
712 chunkContent,
713 {
714 Authorization: uploadParams.authKey,
715 'X-Arguments': uploadArgumentsString,
716 'Content-Type': 'application/octet-stream',
717 },
718 undefined,
719 proxy,
720 );
721
722 if (!uploadPartRes?.ETag) {
723 errorMsg =
724 '上传视频失败,失败原因3:' + uploadPartRes.data?.['X-Errno'] ||
725 '未知错误';
726 console.error(errorMsg);
727 }
728
729 // 上传成功
730 uploadPartInfo.push({
731 PartNumber: i + 1,
732 ETag: uploadPartRes.ETag,
733 });
734 console.log(`第 ${i + 1} 个分片上传成功`);
735
736 return true;
737 }, 3);
738 if (!isSuccess) {
739 throw new Error(errorMsg);
740 }
741 }
742
743 const completeBody = {
744 TransFlag: '0_0',
745 PartInfo: uploadPartInfo,
746 };
747
748 const uploadCompleteRes = await this.uploadFile(
749 this.uploadCompleteUrl + `?UploadID=${uploadId}`,
750 Buffer.from(JSON.stringify(completeBody)),
751 {
752 Authorization: uploadParams.authKey,
753 'Content-Type': 'application/json',
754 'X-Arguments': uploadArgumentsString,
755 },
756 undefined,
757 proxy,
758 );
759
760 if (!uploadCompleteRes.DownloadURL) {
761 throw new Error('上传视频失败,失败原因2:合并分片失败');
762 }
763
764 return uploadCompleteRes.DownloadURL.replace(
765 'http://wxapp.tc.qq.com',
766 'https://finder.video.qq.com',
767 );
768 } catch (err: any) {
769 const errorMessage = err?.message || err || '未知';
770 throw new Error('上传视频失败,失败原因1:' + errorMessage);
771 }
772 }
773
774 /**
775 * 上传封面文件
776 * @param filePath
777 * @param uploadParams
778 */
779 private async uploadCoverFile(
780 filePath: string,
781 uploadParams: any,
782 proxy: string,
783 ): Promise<string> {
784 try {
785 const fileRes = await getFileContent(filePath);
786
787 // 图片数据
788 const fileData = fileRes;
789
790 // 拼接请求参数
791 const uploadArguments: UploadArguments = {
792 apptype: uploadParams.appType,
793 filetype: uploadParams.pictureFileType,
794 weixinnum: uploadParams.uin,
795 filekey: 'finder_video_img.jpeg',
796 filesize: fileData.length,
797 taskid: this.getUniqueTaskId(),
798 scene: uploadParams.scene,
799 };
800
801 let uploadArgumentsString = '';
802 for (const key in uploadArguments) {
803 uploadArgumentsString += `${key}=${uploadArguments[key]}&`;
804 }
805
806 // 获取上传id
807 const uploadIdRes = await this.uploadFile(
808 this.getApplyUploadDfsUrl,
809 Buffer.from(
810 JSON.stringify({
811 BlockPartLength: [fileData.length],
812 BlockSum: 1,
813 }),
814 ),
815 {
816 Authorization: uploadParams.authKey,
817 'Content-Type': 'application/json',
818 'X-Arguments': uploadArgumentsString,
819 },
820 undefined,
821 proxy,
822 );
823
824 if (!uploadIdRes.UploadID) {
825 throw new Error('上传封面失败,失败原因:获取上传id失败');
826 }
827
828 const uploadId = uploadIdRes.UploadID;
829
830 // 开始上传
831 const uploadPartRes = await this.uploadFile(
832 this.uploadpartdfsUrl +
833 `?UploadID=${uploadId}&PartNumber=1&QuickUpload=2`,
834 fileData,
835 {
836 Authorization: uploadParams.authKey,
837 'X-Arguments': uploadArgumentsString,
838 'Content-Type': 'application/octet-stream',
839 },
840 undefined,
841 proxy,
842 );
843
844 if (!uploadPartRes.ETag) {
845 throw new Error(
846 '上传封面失败,失败原因:' + uploadPartRes.data?.['X-Errno'] ||
847 '未知错误',
848 );
849 }
850
851 // 上传成功
852 const uploadPartInfo = [
853 {
854 PartNumber: 1,
855 ETag: uploadPartRes.ETag,
856 },
857 ];
858
859 // 完成分片上传
860 const uploadCompleteRes = await this.uploadFile(
861 this.uploadCompleteUrl + `?UploadID=${uploadId}`,
862 Buffer.from(
863 JSON.stringify({
864 TransFlag: '0_0',
865 PartInfo: uploadPartInfo,
866 }),
867 ),
868 {
869 Authorization: uploadParams.authKey,
870 'Content-Type': 'application/json',
871 'X-Arguments': uploadArgumentsString,
872 },
873 undefined,
874 proxy,
875 );
876
877 if (!uploadCompleteRes.DownloadURL) {
878 throw new Error('上传封面失败,失败原因:合并分片失败');
879 }
880
881 return uploadCompleteRes.DownloadURL;
882 } catch (err: any) {
883 const errorMessage = err?.message || err || '未知';
884 throw new Error('上传封面失败,失败原因:' + errorMessage);
885 }
886 }
887
888 /**
889 * 提交官方剪辑任务
890 */
891 private async postClipUploadVideo(
892 traceKey: string,
893 startUploadTime: number,
894 endUploadTime: number,
895 remoteFileUrl: string,
896 fileInfo: any,
897 filePartInfo: any,
898 cookieString: string,
899 proxy: string,
900 ): Promise<any> {
901 return new Promise(async (resolve, reject) => {
902 try {
903 let videoInfos = null;
904 for (const info of fileInfo.streams) {
905 if (
906 info.hasOwnProperty('codec_type') &&
907 info.codec_type === 'video'
908 ) {
909 videoInfos = info;
910 break;
911 }
912 }
913 if (!videoInfos) {
914 reject('----- postClipUploadVideo ---- jianji');
915 return;
916 }
917 if (videoInfos.duration < 3) {
918 reject('提交剪辑失败,失败原因:视频时长不能小于3秒!');
919 return;
920 }
921 const postClipVideoRes = await this.makeRequest(
922 this.postClipVideoUrl,
923 {
924 method: 'POST',
925 headers: {
926 Cookie: cookieString,
927 'Content-Type': 'application/json',
928 },
929 data: {
930 clipOriginVideoInfo: {
931 width: videoInfos.width,
932 height: videoInfos.height,
933 fileSize: fileInfo.format.size ?? 0,
934 duration: videoInfos.duration,
935 },
936 cropDuration: 0,
937 width: videoInfos.width,
938 height: videoInfos.height,
939 pluginSessionId: null,
940 rawKeyBuff: null,
941 reqScene: 7,
942 scene: 7,
943 targetWidth: videoInfos.height,
944 targetHeight: videoInfos.width,
945 timeStart: 0,
946 timestamp: Date.now(),
947 traceInfo: {
948 traceKey: traceKey,
949 uploadCdnStart: startUploadTime,
950 uploadCdnEnd: endUploadTime,
951 },
952 type: 4,
953 url: remoteFileUrl,
954 useAstraThumbCover: 0,
955 x: 0,
956 y: 0,
957 },
958 dataType: 'json',
959 timeout: 15000,
960 },
961 proxy,
962 );
963 if (postClipVideoRes.errCode !== 0) {
964 reject('提交剪辑失败,失败原因:' + postClipVideoRes.errMsg);
965 return;
966 }
967 const { clipKey } = postClipVideoRes.data;
968 resolve({
969 clipKey: clipKey,
970 url: remoteFileUrl,
971 fileSize: filePartInfo.fileSize,
972 duration: Math.ceil(videoInfos.duration),
973 width: videoInfos.width,
974 height: videoInfos.height,
975 });
976 } catch (err: any) {
977 let errorMessage;
978 if (err && err.message) {
979 errorMessage = err.message;
980 } else if (err) {
981 errorMessage = err;
982 } else {
983 errorMessage = '未知';
984 }
985 reject('提交剪辑任务失败,失败原因:' + errorMessage);
986 }
987 });
988 }
989
990 /**
991 * 创建视频
992 */
993 private async postCreateVideo(
994 cookieString: string,
995 traceKey: string,
996 startUploadTime: number,
997 endUploadTime: number,
998 clipResult: any,
999 platformSetting: any,
1000 proxy: string,
1001 ): Promise<{
1002 lastPublishId: string;
1003 previewVideoLink: string;
1004 }> {
1005 return new Promise(async (resolve, reject) => {
1006 try {
1007 // 处理标题、@好友、话题
1008 let description = platformSetting['title'] ?? '';
1009 const topics = [];
1010 const mentionedUser = [];
1011 if (
1012 platformSetting.hasOwnProperty('topics') &&
1013 platformSetting.topics?.length > 0
1014 ) {
1015 for (const topic of platformSetting.topics) {
1016 description += ` #${topic}`;
1017 topics.push(topic);
1018 }
1019 }
1020 if (
1021 platformSetting.hasOwnProperty('mentionedUserInfo') &&
1022 platformSetting.mentionedUserInfo?.length > 0
1023 ) {
1024 for (const userInfo of platformSetting.mentionedUserInfo) {
1025 if (
1026 userInfo.hasOwnProperty('nickName') &&
1027 userInfo.nickName !== ''
1028 ) {
1029 description += ` @${userInfo.nickName}`;
1030 mentionedUser.push({
1031 nickname: userInfo.nickName,
1032 });
1033 }
1034 }
1035 }
1036 // 处理合集
1037 let topicMix = {};
1038 if (
1039 platformSetting.hasOwnProperty('mixInfo') &&
1040 typeof platformSetting.mixInfo === 'object' &&
1041 platformSetting.mixInfo.hasOwnProperty('mixId') &&
1042 platformSetting.mixInfo.hasOwnProperty('mixName')
1043 ) {
1044 topicMix = {
1045 collectionId: platformSetting.mixInfo['mixId'],
1046 collectionName: platformSetting.mixInfo['mixName'],
1047 };
1048 }
1049 // 处理POI
1050 let poiInfo = {};
1051 if (
1052 platformSetting.hasOwnProperty('poiInfo') &&
1053 typeof platformSetting.poiInfo === 'object' &&
1054 platformSetting.poiInfo.hasOwnProperty('poiId') &&
1055 platformSetting.poiInfo.poiId !== ''
1056 ) {
1057 poiInfo = {
1058 latitude: platformSetting.poiInfo.latitude,
1059 longitude: platformSetting.poiInfo.longitude,
1060 city: platformSetting.poiInfo.poiCity,
1061 poiName: platformSetting.poiInfo.poiName,
1062 address: platformSetting.poiInfo.poiAddress,
1063 poiClassifyId: platformSetting.poiInfo.poiId,
1064 };
1065 }
1066 // 整合请求参数
1067 const requestData = {
1068 objectType: 0,
1069 longitude: 0,
1070 latitude: 0,
1071 feedLongitude: 0,
1072 feedLatitude: 0,
1073 originalFlag: 0,
1074 topics: topics,
1075 isFullPost: 1,
1076 handleFlag: 2,
1077 videoClipTaskId: clipResult['clipKey'],
1078 traceInfo: {
1079 traceKey: traceKey,
1080 uploadCdnStart: startUploadTime,
1081 uploadCdnEnd: endUploadTime,
1082 },
1083 objectDesc: {
1084 mpTitle: '',
1085 description: description,
1086 extReading: {},
1087 mediaType: 4,
1088 location: poiInfo,
1089 topic: topicMix,
1090 event: platformSetting['event'] || {},
1091 mentionedUser: mentionedUser,
1092 media: [
1093 {
1094 url: clipResult['url'],
1095 fileSize: clipResult['fileSize'],
1096 thumbUrl: platformSetting['cover'],
1097 fullThumbUrl: platformSetting['cover'],
1098 mediaType: 4,
1099 videoPlayLen: clipResult['duration'],
1100 width: clipResult['width'],
1101 height: clipResult['height'],
1102 md5sum: this.getUniqueTaskId(),
1103 coverUrl: platformSetting['cover'],
1104 fullCoverUrl: platformSetting['cover'],
1105 urlCdnTaskId: clipResult['clipKey'],
1106 },
1107 ],
1108 postFlag: platformSetting.postFlag || 0,
1109 member: {},
1110 },
1111 postFlag: platformSetting.postFlag || 0,
1112 mode: 1,
1113 clientid: this.getUniqueTaskId(),
1114 timestamp: Date.now(),
1115 rawKeyBuff: null,
1116 pluginSessionId: null,
1117 scene: 7,
1118 reqScene: 7,
1119 };
1120 // 处理定时时间
1121 if (
1122 platformSetting.hasOwnProperty('timingTime') &&
1123 platformSetting.timingTime > Date.now()
1124 ) {
1125 (requestData as any).effectiveTime = Math.floor(
1126 platformSetting.timingTime / 1000,
1127 );
1128 }
1129 // 发起请求
1130 const createRes = await this.makeRequest(
1131 this.postCreateVideoUrl,
1132 {
1133 method: 'POST',
1134 headers: {
1135 Cookie: cookieString,
1136 'Content-Type': 'application/json',
1137 },
1138 data: JSON.stringify(requestData),
1139 dataType: 'json',
1140 timeout: 15000,
1141 },
1142 proxy,
1143 );
1144 if (createRes.errCode !== 0) {
1145 reject('发布失败,失败原因:' + createRes.errMsg);
1146 return;
1147 }
1148 if (
1149 !createRes.data.hasOwnProperty('baseResp') ||
1150 createRes.data.baseResp.errcode !== 0
1151 ) {
1152 reject('发布失败,失败原因:' + createRes.data.baseResp.errmsg);
1153 } else {
1154 const lastWorkInfo = await this.getLastPublishId(cookieString);
1155 resolve(lastWorkInfo);
1156 }
1157 } catch (err: any) {
1158 let errorMessage;
1159 if (err && err.message) {
1160 errorMessage = err.message;
1161 } else if (err) {
1162 errorMessage = err;
1163 } else {
1164 errorMessage = '未知';
1165 }
1166 reject('发布失败,失败原因:' + errorMessage);
1167 }
1168 });
1169 }
1170
1171 /**
1172 * 获取用户视频列表最后一条发布的视频Id
1173 */
1174 private async getLastPublishId(cookieString: string): Promise<{
1175 lastPublishId: string;
1176 previewVideoLink: string;
1177 }> {
1178 const workListRes = await this.makeRequest(
1179 this.getUserWorkListUrl,
1180 {
1181 method: 'POST',
1182 headers: {
1183 Cookie: cookieString,
1184 'Content-Type': 'application/json',
1185 },
1186 data: {
1187 pageSize: 20,
1188 currentPage: 1,
1189 rawKeyBuff: null,
1190 pluginSessionId: null,
1191 scene: 7,
1192 reqScene: 7,
1193 },
1194 },
1195 '',
1196 );
1197
1198 let work;
1199 if (workListRes.errCode === 0) {
1200 const workList = workListRes.data.list;
1201 if (workList.length > 0) {
1202 work = workList[0];
1203 }
1204 }
1205
1206 // 获取预览的视频链接
1207 const previewVideoRes = await this.makeRequest(
1208 'https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/get_object_short_link',
1209 {
1210 method: 'POST',
1211 headers: {
1212 Cookie: cookieString,
1213 'Content-Type': 'application/json',
1214 },
1215 data: {
1216 exportId: work.exportId,
1217 nonceId: work.objectNonce,
1218 },
1219 },
1220 '',
1221 );
1222
1223 return {
1224 lastPublishId: work?.desc?.media[0]?.md5sum ?? '',
1225 previewVideoLink: previewVideoRes,
1226 };
1227 }
1228
1229 /**
1230 * 视频作品发布
1231 */
1232 async publishVideoWorkApi(
1233 cookies: Electron.Cookie[],
1234 filePath: string,
1235 platformSetting: {
1236 cover: string;
1237 title?: string;
1238 topics?: string[];
1239 des?: string;
1240 timingTime?: number;
1241 // 合集
1242 mixInfo?: {
1243 mixId: string;
1244 mixName: string;
1245 };
1246 // 位置信息
1247 poiInfo?: {
1248 latitude: number;
1249 longitude: number;
1250 poiCity: string;
1251 poiName: string;
1252 poiAddress: string;
1253 poiId: string;
1254 };
1255 // @的用户
1256 mentionedUserInfo?: {
1257 nickName?: string;
1258 }[];
1259 // 活动
1260 event?: {
1261 eventCreatorNickname: string;
1262 eventTopicId: string;
1263 eventName: string;
1264 };
1265 // 0=非原创 1=原创
1266 postFlag: 0 | 1;
1267 proxy: string;
1268 },
1269 callback: (progress: number, msg?: string) => void,
1270 ): Promise<{
1271 publishTime: number;
1272 publishId: string;
1273 shareLink: string;
1274 }> {
1275 this.callback = callback;
1276 console.log('微信视频号最初发布参数:', {
1277 platformSetting,
1278 filePath,
1279 });
1280 callback(5, '加载中...');
1281 const fileInfo = await FileUtils.getFileInfo(filePath);
1282 callback(10);
1283 const cookieString = CommonUtils.convertCookieToJson(cookies);
1284 callback(15);
1285 const traceKey = await this.getPublishTraceKey(
1286 cookieString,
1287 platformSetting.proxy,
1288 );
1289 callback(20);
1290 const uploadParams = await this.getPublishUploadParams(
1291 cookieString,
1292 platformSetting.proxy,
1293 );
1294 callback(26);
1295 const filePartInfo = await FileUtils.getFilePartInfo(
1296 filePath,
1297 this.fileBlockSize,
1298 );
1299 const startUploadTime = Math.floor(Date.now() / 1000);
1300 const remoteVideoUrl = await this.uploadVideoFile(
1301 filePath,
1302 uploadParams,
1303 filePartInfo,
1304 platformSetting.proxy,
1305 );
1306 callback(30);
1307 const endUploadTime = Math.floor(Date.now() / 1000);
1308
1309 callback(40, '正在上传视频...');
1310 const clipResult = await this.postClipUploadVideo(
1311 traceKey,
1312 startUploadTime,
1313 endUploadTime,
1314 remoteVideoUrl,
1315 fileInfo,
1316 filePartInfo,
1317 cookieString,
1318 platformSetting.proxy,
1319 );
1320 callback(60, '正在上传封面...');
1321 platformSetting.cover = await this.uploadCoverFile(
1322 platformSetting.cover,
1323 uploadParams,
1324 platformSetting.proxy,
1325 );
1326 callback(80, '正在发布视频...');
1327
1328 const lastParams = {
1329 ...platformSetting,
1330 cover: platformSetting.cover,
1331 title: platformSetting.title,
1332 topics: platformSetting.topics ? platformSetting.topics : [],
1333 };
1334 console.log('视频号最终发布参数:', lastParams);
1335 const lastPublishRes = await this.postCreateVideo(
1336 cookieString,
1337 traceKey,
1338 startUploadTime,
1339 endUploadTime,
1340 clipResult,
1341 lastParams,
1342 platformSetting.proxy,
1343 );
1344 callback(100);
1345
1346 // 返回成功
1347 return {
1348 publishTime: Math.floor(Date.now() / 1000),
1349 publishId: lastPublishRes.lastPublishId,
1350 shareLink: lastPublishRes.previewVideoLink,
1351 };
1352 }
1353
1354 // 获取位置数据
1355 async getLocation(params: {
1356 query: string;
1357 longitude: number;
1358 latitude: number;
1359 cookie: Electron.Cookie[];
1360 }) {
1361 return await requestNet<WeChatLocationData>({
1362 url: `https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/helper/helper_search_location`,
1363 method: 'POST',
1364 body: {
1365 ...params,
1366 reqScene: 7,
1367 scene: 7,
1368 },
1369 headers: {
1370 cookie: CookieToString(params.cookie),
1371 },
1372 });
1373 }
1374
1375 // 获取视频号的活动
1376 async getActivityList(params: { cookie: Electron.Cookie[]; query: string }) {
1377 return await requestNet<WeChatVideoApiResponse>({
1378 url: `https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/post/post_search_event`,
1379 method: 'POST',
1380 body: params,
1381 headers: {
1382 cookie: CookieToString(params.cookie),
1383 },
1384 });
1385 }
1386
1387 // 获取@用户列表
1388 async getUsers(cookie: Electron.Cookie[], keyword: string, page: number) {
1389 return await requestNet<WeChatVideoUserData>({
1390 url: `https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/helper/helper_search_finder_account`,
1391 headers: {
1392 cookie: CookieToString(cookie),
1393 },
1394 method: 'POST',
1395 body: {
1396 pageSize: 10,
1397 currentPage: page,
1398 offset: (page - 1) * 10,
1399 query: keyword,
1400 },
1401 });
1402 }
1403
1404 // 获取合集列表
1405 async getMixList(cookie: Electron.Cookie[]) {
1406 return await requestNet<WxSPHGetMixListResponse>({
1407 url: `https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/collection/get_collection_list`,
1408 headers: {
1409 cookie: CookieToString(cookie),
1410 },
1411 method: 'POST',
1412 body: {
1413 pageNum: 1,
1414 pageSize: 200,
1415 },
1416 });
1417 }
1418
1419 /**
1420 * 获取作品列表
1421 * @returns
1422 */
1423 async getPostList(
1424 cookie: Electron.Cookie[],
1425 pageInfo: { pageNo: number; pageSize: number },
1426 ) {
1427 const res = await requestNet<SphGetPostListResponse>({
1428 url: `https://channels.weixin.qq.com/micro/interaction/cgi-bin/mmfinderassistant-bin/post/post_list?_rid=67d03155-a6da7281`,
1429 headers: {
1430 cookie: CookieToString(cookie),
1431 },
1432 method: 'POST',
1433 body: {
1434 currentPage: pageInfo.pageNo,
1435 forMcn: false,
1436 needAllCommentCount: true,
1437 onlyUnread: false,
1438 pageSize: pageInfo.pageSize,
1439 pluginSessionId: null,
1440 rawKeyBuff: null,
1441 reqScene: 7, // 固定值
1442 scene: 7, // 固定值
1443 timestamp: Date.now() + '', // '1741697365389',
1444 userpageType: 13,
1445 _log_finder_id:
1446 'v2_060000231003b20faec8c5e38b10cbd6cb06ef3cb077ad5b14a8587570bc414e95c4b7e034ea@finder',
1447 _log_finder_uin: '',
1448 },
1449 });
1450
1451 const ret = res.data.data;
1452 return ret;
1453 }
1454
1455 /**
1456 * 获取评论列表
1457 * @param cookie
1458 * @param exportId
1459 * @returns
1460 */
1461 async getCommentList(cookie: Electron.Cookie[], exportId: string) {
1462 const res = await requestNet<SphGetCommentListResponse>({
1463 url: `https://channels.weixin.qq.com/micro/interaction/cgi-bin/mmfinderassistant-bin/comment/comment_list?_rid=67d032a7-6c8f7126`,
1464 headers: {
1465 cookie: CookieToString(cookie),
1466 },
1467 method: 'POST',
1468 body: {
1469 commentSelection: false,
1470 exportId,
1471 forMcn: false,
1472 lastBuff: '',
1473 pluginSessionId: null,
1474 rawKeyBuff: null,
1475 reqScene: 7,
1476 scene: 7,
1477 timestamp: Date.now() + '',
1478 _log_finder_id:
1479 'v2_060000231003b20faec8c5e38b10cbd6cb06ef3cb077ad5b14a8587570bc414e95c4b7e034ea@finder',
1480 _log_finder_uin: '',
1481 },
1482 });
1483
1484 const ret = res.data.data;
1485 return ret;
1486 }
1487
1488 /**
1489 * 评论作品和回复评论
1490 * @param cookie
1491 * @param exportId
1492 * @param content
1493 * @param comment
1494 * @returns
1495 */
1496 async createComment(
1497 cookie: Electron.Cookie[],
1498 exportId: string,
1499 content: string,
1500 comment: Partial<CommentInfo> = {},
1501 ) {
1502 const res = await requestNet<WeChatVideoUserData>({
1503 url: `https://channels.weixin.qq.com/micro/interaction/cgi-bin/mmfinderassistant-bin/comment/create_comment?_rid=67d032a7-6c8f7126`,
1504 headers: {
1505 cookie: CookieToString(cookie),
1506 },
1507 method: 'POST',
1508 body: {
1509 clientId: uuidv4(),
1510 comment, // 默认{}
1511 content,
1512 exportId,
1513 pluginSessionId: null,
1514 rawKeyBuff: null,
1515 replyCommentId: comment?.commentId || '',
1516 reqScene: 7,
1517 rootCommentId: comment?.replyCommentId || '',
1518 scene: 7,
1519 timestamp: Date.now() + '',
1520 _log_finder_id:
1521 'v2_060000231003b20faec8c5e38b10cbd6cb06ef3cb077ad5b14a8587570bc414e95c4b7e034ea@finder',
1522 _log_finder_uin: '',
1523 },
1524 });
1525
1526 return res;
1527 }
1528 }
1529
1530 // 导出服务实例
1531 export const shipinhaoService = new ShipinhaoService();
1532
1532 lines TYPESCRIPT