CommentService.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. import { AppDataSource, Comment, PlatformAccount, Work } from '../models/index.js';
  2. import { AppError } from '../middleware/error.js';
  3. import { ERROR_CODES, HTTP_STATUS, WS_EVENTS } from '@media-manager/shared';
  4. import type {
  5. Comment as CommentType,
  6. CommentStats,
  7. PaginatedData,
  8. PlatformType,
  9. } from '@media-manager/shared';
  10. import { wsManager } from '../websocket/index.js';
  11. import { headlessBrowserService, type CookieData } from './HeadlessBrowserService.js';
  12. import { CookieManager } from '../automation/cookie.js';
  13. import { logger } from '../utils/logger.js';
  14. interface GetCommentsParams {
  15. page: number;
  16. pageSize: number;
  17. accountId?: number;
  18. workId?: number;
  19. platform?: string;
  20. isRead?: boolean;
  21. keyword?: string;
  22. }
  23. export class CommentService {
  24. private commentRepository = AppDataSource.getRepository(Comment);
  25. async getComments(userId: number, params: GetCommentsParams): Promise<PaginatedData<CommentType>> {
  26. const { page, pageSize, accountId, workId, platform, isRead, keyword } = params;
  27. const skip = (page - 1) * pageSize;
  28. const queryBuilder = this.commentRepository
  29. .createQueryBuilder('comment')
  30. .where('comment.userId = :userId', { userId });
  31. if (accountId) {
  32. queryBuilder.andWhere('comment.accountId = :accountId', { accountId });
  33. }
  34. if (workId) {
  35. // 直接使用 workId 查询
  36. queryBuilder.andWhere('comment.workId = :workId', { workId });
  37. logger.info(`Querying comments for workId: ${workId}`);
  38. }
  39. if (platform) {
  40. queryBuilder.andWhere('comment.platform = :platform', { platform });
  41. }
  42. if (isRead !== undefined) {
  43. queryBuilder.andWhere('comment.isRead = :isRead', { isRead });
  44. }
  45. if (keyword) {
  46. queryBuilder.andWhere(
  47. '(comment.content LIKE :keyword OR comment.authorName LIKE :keyword)',
  48. { keyword: `%${keyword}%` }
  49. );
  50. }
  51. // 打印查询 SQL 用于调试
  52. logger.info(`Comment query: userId=${userId}, accountId=${accountId}, workId=${workId}, platform=${platform}`);
  53. const [comments, total] = await queryBuilder
  54. .orderBy('comment.commentTime', 'DESC')
  55. .skip(skip)
  56. .take(pageSize)
  57. .getManyAndCount();
  58. logger.info(`Found ${total} comments`);
  59. return {
  60. items: comments.map(this.formatComment),
  61. total,
  62. page,
  63. pageSize,
  64. totalPages: Math.ceil(total / pageSize),
  65. };
  66. }
  67. async getStats(userId: number): Promise<CommentStats> {
  68. const totalCount = await this.commentRepository.count({ where: { userId } });
  69. const unreadCount = await this.commentRepository.count({ where: { userId, isRead: false } });
  70. const unrepliedCount = await this.commentRepository
  71. .createQueryBuilder('comment')
  72. .where('comment.userId = :userId', { userId })
  73. .andWhere('comment.replyContent IS NULL')
  74. .getCount();
  75. const today = new Date();
  76. today.setHours(0, 0, 0, 0);
  77. const todayCount = await this.commentRepository
  78. .createQueryBuilder('comment')
  79. .where('comment.userId = :userId', { userId })
  80. .andWhere('comment.createdAt >= :today', { today })
  81. .getCount();
  82. return { totalCount, unreadCount, unrepliedCount, todayCount };
  83. }
  84. async markAsRead(userId: number, commentIds: number[]): Promise<void> {
  85. await this.commentRepository
  86. .createQueryBuilder()
  87. .update(Comment)
  88. .set({ isRead: true })
  89. .where('userId = :userId AND id IN (:...commentIds)', { userId, commentIds })
  90. .execute();
  91. }
  92. async replyComment(userId: number, commentId: number, content: string): Promise<CommentType> {
  93. const comment = await this.commentRepository.findOne({
  94. where: { id: commentId, userId },
  95. });
  96. if (!comment) {
  97. throw new AppError('评论不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.COMMENT_NOT_FOUND);
  98. }
  99. // TODO: 调用平台适配器发送回复
  100. // 这里只更新本地记录
  101. await this.commentRepository.update(commentId, {
  102. replyContent: content,
  103. repliedAt: new Date(),
  104. isRead: true,
  105. });
  106. const updated = await this.commentRepository.findOne({ where: { id: commentId } });
  107. wsManager.sendToUser(userId, WS_EVENTS.COMMENT_REPLIED, {
  108. comment: this.formatComment(updated!),
  109. });
  110. return this.formatComment(updated!);
  111. }
  112. async batchReply(
  113. userId: number,
  114. commentIds: number[],
  115. content: string
  116. ): Promise<{ success: number; failed: number }> {
  117. let success = 0;
  118. let failed = 0;
  119. for (const commentId of commentIds) {
  120. try {
  121. await this.replyComment(userId, commentId, content);
  122. success++;
  123. } catch {
  124. failed++;
  125. }
  126. }
  127. return { success, failed };
  128. }
  129. /**
  130. * 异步同步评论(后台执行,通过 WebSocket 通知结果)
  131. */
  132. syncCommentsAsync(userId: number, accountId?: number): void {
  133. // 通知用户同步已开始 - 直接使用字符串,并在 payload 中加入 event 字段
  134. wsManager.sendToUser(userId, 'comment:sync_started', {
  135. event: 'sync_started',
  136. accountId,
  137. message: '正在同步评论...',
  138. });
  139. // 进度回调:在处理每个作品时发送进度更新
  140. const onProgress = (current: number, total: number, workTitle: string) => {
  141. const progress = total > 0 ? Math.round((current / total) * 100) : 0;
  142. wsManager.sendToUser(userId, 'comment:sync_progress', {
  143. event: 'sync_progress',
  144. accountId,
  145. current,
  146. total,
  147. progress,
  148. workTitle,
  149. message: `正在同步: ${workTitle || `作品 ${current}/${total}`}`,
  150. });
  151. };
  152. // 后台执行同步任务
  153. this.syncComments(userId, accountId, onProgress)
  154. .then((result) => {
  155. logger.info(`Comment sync completed: synced ${result.synced} comments from ${result.accounts} accounts`);
  156. // 同步完成,通知用户
  157. wsManager.sendToUser(userId, 'comment:synced', {
  158. event: 'synced',
  159. accountId,
  160. syncedCount: result.synced,
  161. accountCount: result.accounts,
  162. message: `同步完成,共同步 ${result.synced} 条评论`,
  163. });
  164. })
  165. .catch((error) => {
  166. logger.error('Comment sync failed:', error);
  167. // 同步失败,通知用户
  168. wsManager.sendToUser(userId, 'comment:sync_failed', {
  169. event: 'sync_failed',
  170. accountId,
  171. message: error instanceof Error ? error.message : '同步失败,请稍后重试',
  172. });
  173. });
  174. }
  175. /**
  176. * 同步指定账号的评论
  177. * @param onProgress 进度回调 (current, total, workTitle)
  178. */
  179. async syncComments(
  180. userId: number,
  181. accountId?: number,
  182. onProgress?: (current: number, total: number, workTitle: string) => void
  183. ): Promise<{ synced: number; accounts: number }> {
  184. const accountRepository = AppDataSource.getRepository(PlatformAccount);
  185. // 获取需要同步的账号列表
  186. const whereCondition: { userId: number; id?: number; platform?: PlatformType } = { userId };
  187. if (accountId) {
  188. whereCondition.id = accountId;
  189. }
  190. const accounts = await accountRepository.find({ where: whereCondition });
  191. if (accounts.length === 0) {
  192. throw new AppError('没有找到可同步的账号', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
  193. }
  194. let totalSynced = 0;
  195. let syncedAccounts = 0;
  196. for (const account of accounts) {
  197. try {
  198. // 只处理支持的平台
  199. if (account.platform !== 'douyin' && account.platform !== 'xiaohongshu' && account.platform !== 'weixin_video' && account.platform !== 'baijiahao') {
  200. logger.info(`Skipping unsupported platform: ${account.platform}`);
  201. continue;
  202. }
  203. // 解密 Cookie
  204. if (!account.cookieData) {
  205. logger.warn(`Account ${account.id} has no cookies`);
  206. continue;
  207. }
  208. let decryptedCookies: string;
  209. try {
  210. decryptedCookies = CookieManager.decrypt(account.cookieData);
  211. } catch {
  212. decryptedCookies = account.cookieData;
  213. }
  214. // 解析 Cookie - 支持两种格式
  215. let cookies: CookieData[];
  216. try {
  217. // 先尝试 JSON 格式
  218. cookies = JSON.parse(decryptedCookies);
  219. } catch {
  220. // 如果 JSON 解析失败,尝试解析 "name=value; name2=value2" 格式
  221. cookies = this.parseCookieString(decryptedCookies, account.platform);
  222. if (cookies.length === 0) {
  223. logger.error(`Invalid cookie format for account ${account.id}`);
  224. continue;
  225. }
  226. }
  227. // 获取评论数据 - 根据平台类型调用不同方法
  228. logger.info(`Syncing comments for account ${account.id} (${account.platform})...`);
  229. let workComments: Array<{
  230. videoId: string;
  231. videoTitle: string;
  232. comments: Array<{
  233. commentId: string;
  234. authorId: string;
  235. authorName: string;
  236. authorAvatar: string;
  237. content: string;
  238. likeCount: number;
  239. commentTime: string;
  240. replyCount?: number;
  241. parentCommentId?: string;
  242. }>;
  243. }> = [];
  244. if (account.platform === 'douyin') {
  245. workComments = await headlessBrowserService.fetchDouyinCommentsViaApi(cookies);
  246. } else if (account.platform === 'xiaohongshu') {
  247. workComments = await headlessBrowserService.fetchXiaohongshuCommentsViaApi(cookies);
  248. } else if (account.platform === 'weixin_video') {
  249. workComments = await headlessBrowserService.fetchWeixinVideoCommentsViaApi(cookies);
  250. } else if (account.platform === 'baijiahao') {
  251. workComments = await headlessBrowserService.fetchBaijiahaoCommentsViaApi(cookies);
  252. }
  253. // 获取该账号的所有作品,用于关联
  254. const workRepository = AppDataSource.getRepository(Work);
  255. const accountWorks = await workRepository.find({
  256. where: { userId, accountId: account.id },
  257. });
  258. logger.info(`Found ${accountWorks.length} works for account ${account.id}`);
  259. // 保存评论到数据库
  260. let accountSynced = 0;
  261. const totalWorks = workComments.length;
  262. // 创建 platformVideoId -> workId 的快速映射
  263. const videoIdToWorkMap = new Map<string, { id: number; title: string }>();
  264. for (const work of accountWorks) {
  265. if (work.platformVideoId) {
  266. videoIdToWorkMap.set(work.platformVideoId, { id: work.id, title: work.title });
  267. // 同时存储带平台前缀的版本(兼容 comment.videoId 为 "platform_xxx" 的情况)
  268. videoIdToWorkMap.set(`${work.platform}_${work.platformVideoId}`, { id: work.id, title: work.title });
  269. // 同时存储不带前缀的版本(如果 platformVideoId 是 "platform_xxx" 格式)
  270. if (work.platformVideoId.includes('_')) {
  271. const parts = work.platformVideoId.split('_');
  272. if (parts.length >= 2) {
  273. videoIdToWorkMap.set(parts.slice(1).join('_'), { id: work.id, title: work.title });
  274. }
  275. }
  276. }
  277. }
  278. logger.info(`Created videoId mapping with ${videoIdToWorkMap.size} entries`);
  279. for (let workIndex = 0; workIndex < workComments.length; workIndex++) {
  280. const workComment = workComments[workIndex];
  281. // 发送进度更新
  282. if (onProgress) {
  283. onProgress(workIndex + 1, totalWorks, workComment.videoTitle || `作品 ${workIndex + 1}`);
  284. }
  285. let workId: number | null = null;
  286. const commentVideoId = workComment.videoId?.toString() || '';
  287. const commentVideoTitle = workComment.videoTitle?.trim() || '';
  288. logger.info(`Trying to match work for videoId: "${commentVideoId}", title: "${commentVideoTitle}"`);
  289. // 1. 【首选】通过 platformVideoId (aweme_id) 匹配 - 最可靠的方式
  290. if (commentVideoId) {
  291. // 直接匹配
  292. if (videoIdToWorkMap.has(commentVideoId)) {
  293. const matched = videoIdToWorkMap.get(commentVideoId)!;
  294. workId = matched.id;
  295. logger.info(`Matched work by videoId: ${commentVideoId} -> workId: ${workId}, title: "${matched.title}"`);
  296. }
  297. // 尝试带平台前缀匹配
  298. if (!workId) {
  299. const prefixedId = `${account.platform}_${commentVideoId}`;
  300. if (videoIdToWorkMap.has(prefixedId)) {
  301. const matched = videoIdToWorkMap.get(prefixedId)!;
  302. workId = matched.id;
  303. logger.info(`Matched work by prefixed videoId: ${prefixedId} -> workId: ${workId}`);
  304. }
  305. }
  306. // 遍历匹配(处理各种格式)
  307. if (!workId) {
  308. const matchedWork = accountWorks.find(w => {
  309. if (!w.platformVideoId) return false;
  310. // 尝试各种匹配方式
  311. return w.platformVideoId === commentVideoId ||
  312. w.platformVideoId === `${account.platform}_${commentVideoId}` ||
  313. w.platformVideoId.endsWith(`_${commentVideoId}`) ||
  314. w.platformVideoId.includes(commentVideoId);
  315. });
  316. if (matchedWork) {
  317. workId = matchedWork.id;
  318. logger.info(`Matched work by videoId iteration: ${commentVideoId} -> workId: ${workId}, platformVideoId: ${matchedWork.platformVideoId}`);
  319. }
  320. }
  321. }
  322. // 2. 通过标题精确匹配
  323. if (!workId && commentVideoTitle) {
  324. let matchedWork = accountWorks.find(w => {
  325. if (!w.title) return false;
  326. return w.title.trim() === commentVideoTitle;
  327. });
  328. // 去除空白字符后匹配
  329. if (!matchedWork) {
  330. const normalizedCommentTitle = commentVideoTitle.replace(/\s+/g, '');
  331. matchedWork = accountWorks.find(w => {
  332. if (!w.title) return false;
  333. return w.title.trim().replace(/\s+/g, '') === normalizedCommentTitle;
  334. });
  335. }
  336. // 包含匹配
  337. if (!matchedWork) {
  338. matchedWork = accountWorks.find(w => {
  339. if (!w.title) return false;
  340. const workTitle = w.title.trim();
  341. const shortCommentTitle = commentVideoTitle.slice(0, 50);
  342. const shortWorkTitle = workTitle.slice(0, 50);
  343. return workTitle.includes(shortCommentTitle) ||
  344. commentVideoTitle.includes(shortWorkTitle) ||
  345. shortWorkTitle.includes(shortCommentTitle) ||
  346. shortCommentTitle.includes(shortWorkTitle);
  347. });
  348. }
  349. // 模糊匹配
  350. if (!matchedWork) {
  351. const normalizeTitle = (title: string) => {
  352. return title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '').toLowerCase();
  353. };
  354. const normalizedCommentTitle = normalizeTitle(commentVideoTitle);
  355. matchedWork = accountWorks.find(w => {
  356. if (!w.title) return false;
  357. const normalizedWorkTitle = normalizeTitle(w.title);
  358. return normalizedWorkTitle.slice(0, 40) === normalizedCommentTitle.slice(0, 40) ||
  359. normalizedWorkTitle.includes(normalizedCommentTitle.slice(0, 30)) ||
  360. normalizedCommentTitle.includes(normalizedWorkTitle.slice(0, 30));
  361. });
  362. }
  363. if (matchedWork) {
  364. workId = matchedWork.id;
  365. logger.info(`Matched work by title: "${matchedWork.title}" -> workId: ${workId}`);
  366. }
  367. }
  368. // 3. 如果只有一个作品,直接关联
  369. if (!workId && accountWorks.length === 1) {
  370. workId = accountWorks[0].id;
  371. logger.info(`Only one work, using default: workId: ${workId}`);
  372. }
  373. logger.info(`Final work mapping: videoId="${commentVideoId}", title="${commentVideoTitle}", workId=${workId}`);
  374. for (const comment of workComment.comments) {
  375. try {
  376. // 过滤无效评论内容 - 放宽限制,只过滤纯操作按钮文本
  377. if (!comment.content ||
  378. /^(回复|删除|举报|点赞|分享|收藏)$/.test(comment.content.trim())) {
  379. logger.debug(`Skipping invalid comment content: ${comment.content}`);
  380. continue;
  381. }
  382. // 检查评论是否已存在(基于内容+作者+账号的去重)
  383. const existing = await this.commentRepository
  384. .createQueryBuilder('comment')
  385. .where('comment.accountId = :accountId', { accountId: account.id })
  386. .andWhere('comment.authorName = :authorName', { authorName: comment.authorName })
  387. .andWhere('comment.content = :content', { content: comment.content })
  388. .getOne();
  389. if (!existing) {
  390. const newComment = this.commentRepository.create({
  391. userId,
  392. accountId: account.id,
  393. workId, // 关联作品 ID
  394. platform: account.platform as PlatformType,
  395. videoId: workComment.videoId,
  396. commentId: comment.commentId,
  397. parentCommentId: comment.parentCommentId,
  398. authorId: comment.authorId,
  399. authorName: comment.authorName,
  400. authorAvatar: comment.authorAvatar,
  401. content: comment.content,
  402. likeCount: comment.likeCount,
  403. commentTime: comment.commentTime ? new Date(comment.commentTime) : new Date(),
  404. isRead: false,
  405. isTop: false,
  406. });
  407. await this.commentRepository.save(newComment);
  408. accountSynced++;
  409. logger.info(`Saved comment: "${comment.content.slice(0, 30)}..." -> workId: ${workId}`);
  410. } else {
  411. // 如果评论已存在但没有 workId,更新它
  412. if (!existing.workId && workId) {
  413. await this.commentRepository.update(existing.id, { workId });
  414. logger.info(`Updated existing comment workId: ${existing.id} -> ${workId}`);
  415. }
  416. }
  417. } catch (saveError) {
  418. logger.warn(`Failed to save comment ${comment.commentId}:`, saveError);
  419. }
  420. }
  421. }
  422. if (accountSynced > 0) {
  423. totalSynced += accountSynced;
  424. syncedAccounts++;
  425. logger.info(`Synced ${accountSynced} comments for account ${account.id}`);
  426. // 注意:不在这里发送 COMMENT_SYNCED,而是由 syncCommentsAsync 统一发送
  427. }
  428. } catch (accountError) {
  429. logger.error(`Failed to sync comments for account ${account.id}:`, accountError);
  430. }
  431. }
  432. // 尝试修复没有 workId 的现有评论
  433. await this.fixOrphanedComments(userId);
  434. return { synced: totalSynced, accounts: syncedAccounts };
  435. }
  436. /**
  437. * 修复没有 workId 的评论
  438. */
  439. private async fixOrphanedComments(userId: number): Promise<void> {
  440. try {
  441. const workRepository = AppDataSource.getRepository(Work);
  442. // 获取所有没有 workId 的评论
  443. const orphanedComments = await this.commentRepository.find({
  444. where: { userId, workId: undefined as unknown as number },
  445. });
  446. if (orphanedComments.length === 0) return;
  447. logger.info(`Found ${orphanedComments.length} comments without workId, trying to fix...`);
  448. // 获取用户的所有作品
  449. const works = await workRepository.find({ where: { userId } });
  450. // 创建多种格式的 videoId -> workId 映射
  451. const videoIdToWork = new Map<string, { id: number; title: string }>();
  452. for (const work of works) {
  453. if (work.platformVideoId) {
  454. // 存储原始 platformVideoId
  455. videoIdToWork.set(work.platformVideoId, { id: work.id, title: work.title });
  456. // 存储带平台前缀的版本
  457. videoIdToWork.set(`${work.platform}_${work.platformVideoId}`, { id: work.id, title: work.title });
  458. // 如果是 "platform_xxx" 格式,也存储纯 ID
  459. if (work.platformVideoId.includes('_')) {
  460. const parts = work.platformVideoId.split('_');
  461. if (parts.length >= 2) {
  462. const pureId = parts.slice(1).join('_');
  463. videoIdToWork.set(pureId, { id: work.id, title: work.title });
  464. }
  465. }
  466. // 如果是纯数字 ID,也存储带前缀的版本
  467. if (/^\d+$/.test(work.platformVideoId)) {
  468. videoIdToWork.set(`${work.platform}_${work.platformVideoId}`, { id: work.id, title: work.title });
  469. }
  470. }
  471. }
  472. let fixedCount = 0;
  473. for (const comment of orphanedComments) {
  474. let matchedWorkId: number | null = null;
  475. // 1. 尝试通过 videoId 精确匹配
  476. if (comment.videoId) {
  477. if (videoIdToWork.has(comment.videoId)) {
  478. matchedWorkId = videoIdToWork.get(comment.videoId)!.id;
  479. }
  480. // 尝试带前缀匹配
  481. if (!matchedWorkId) {
  482. const prefixedId = `${comment.platform}_${comment.videoId}`;
  483. if (videoIdToWork.has(prefixedId)) {
  484. matchedWorkId = videoIdToWork.get(prefixedId)!.id;
  485. }
  486. }
  487. // 尝试去掉前缀匹配
  488. if (!matchedWorkId && comment.videoId.includes('_')) {
  489. const pureId = comment.videoId.split('_').pop()!;
  490. if (videoIdToWork.has(pureId)) {
  491. matchedWorkId = videoIdToWork.get(pureId)!.id;
  492. }
  493. }
  494. // 遍历查找包含关系
  495. if (!matchedWorkId) {
  496. const matchedWork = works.find(w =>
  497. w.platformVideoId?.includes(comment.videoId!) ||
  498. comment.videoId!.includes(w.platformVideoId || '')
  499. );
  500. if (matchedWork) {
  501. matchedWorkId = matchedWork.id;
  502. }
  503. }
  504. }
  505. // 2. 尝试通过账号匹配(如果该账号只有一个作品)
  506. if (!matchedWorkId) {
  507. const accountWorks = works.filter(w => w.accountId === comment.accountId);
  508. if (accountWorks.length === 1) {
  509. matchedWorkId = accountWorks[0].id;
  510. }
  511. }
  512. if (matchedWorkId) {
  513. await this.commentRepository.update(comment.id, { workId: matchedWorkId });
  514. fixedCount++;
  515. logger.info(`Fixed comment ${comment.id} (videoId: ${comment.videoId}) -> workId: ${matchedWorkId}`);
  516. }
  517. }
  518. logger.info(`Fixed ${fixedCount}/${orphanedComments.length} orphaned comments`);
  519. } catch (error) {
  520. logger.warn('Failed to fix orphaned comments:', error);
  521. }
  522. }
  523. /**
  524. * 将 cookie 字符串解析为 cookie 列表
  525. */
  526. private parseCookieString(cookieString: string, platform: string): CookieData[] {
  527. // 获取平台对应的域名
  528. const domainMap: Record<string, string> = {
  529. douyin: '.douyin.com',
  530. kuaishou: '.kuaishou.com',
  531. xiaohongshu: '.xiaohongshu.com',
  532. weixin_video: '.qq.com',
  533. bilibili: '.bilibili.com',
  534. toutiao: '.toutiao.com',
  535. baijiahao: '.baidu.com',
  536. qie: '.qq.com',
  537. dayuhao: '.alibaba.com',
  538. };
  539. const domain = domainMap[platform] || `.${platform}.com`;
  540. // 解析 "name=value; name2=value2" 格式的 cookie 字符串
  541. const cookies: CookieData[] = [];
  542. const pairs = cookieString.split(';');
  543. for (const pair of pairs) {
  544. const trimmed = pair.trim();
  545. if (!trimmed) continue;
  546. const eqIndex = trimmed.indexOf('=');
  547. if (eqIndex === -1) continue;
  548. const name = trimmed.substring(0, eqIndex).trim();
  549. const value = trimmed.substring(eqIndex + 1).trim();
  550. if (name && value) {
  551. cookies.push({
  552. name,
  553. value,
  554. domain,
  555. path: '/',
  556. });
  557. }
  558. }
  559. return cookies;
  560. }
  561. private formatComment(comment: Comment): CommentType {
  562. return {
  563. id: comment.id,
  564. userId: comment.userId,
  565. accountId: comment.accountId,
  566. workId: comment.workId || undefined,
  567. platform: comment.platform!,
  568. videoId: comment.videoId || '',
  569. platformVideoUrl: comment.platformVideoUrl,
  570. commentId: comment.commentId,
  571. parentCommentId: comment.parentCommentId,
  572. authorId: comment.authorId || '',
  573. authorName: comment.authorName || '',
  574. authorAvatar: comment.authorAvatar,
  575. content: comment.content || '',
  576. likeCount: comment.likeCount,
  577. replyContent: comment.replyContent,
  578. repliedAt: comment.repliedAt?.toISOString() || null,
  579. isRead: comment.isRead,
  580. isTop: comment.isTop,
  581. commentTime: comment.commentTime?.toISOString() || '',
  582. createdAt: comment.createdAt.toISOString(),
  583. };
  584. }
  585. }